Java语言基础
文章目录
1、Java概述
Java是一门面向对象编程语言。
JVM、JRE和JDK区别
- JVM(Java Virtual Machine):Java虚拟机,Java程序需要运行在虚拟机上,不同平台有自己的虚拟机,因而可以跨平台
- JRE(Java Runtime Environment):包括java虚拟机和Java程序所需的核心类库
- JDK(Java Development Kit):开发人员使用的,其中包括java的开发工具、JRE。其中开发工具:编译工具(javac.exe),打包工具(jar.exe)
JDK三大版本
- Java SE:标准版、允许开发和部署在桌面、服务器、嵌入式环境和实时环境中使用的Java应用程序。
- Java EE:企业版、企业级开发和部署可移植、健壮、可伸缩且安全的服务端Java应用程序。JavaEE是在JavaSE的基础上构建的,它提供Web服务、组件模型、管理和通信API,可以用来实现企业级的面向服务体系结构和Web2.0应用程序。
- Java ME:微型版、JavaME为在移动设备和嵌入式设备上运行的应用程序提供一个健壮且灵活的环境。
Java语言的特点
- 面向对象(封装、继承、多态)
- 跨平台
- 支持网络编程
- 支持多线程
- 健壮性、Java语言强类型机制、异常处理、垃圾回收机制
- 安全性
跨平台特性、原理
- 跨平台:Java语言编写的程序,一次编译后,可以在多个系统平台上运行
- 实现原理:Java程序通过Java虚拟机在系统平台上运行的,只要该系统安装相应的Java虚拟机,该系统就可以运行Java程序,从而实现应用程序可以容易的跨平台运行
字节码
java源代码经过虚拟机编译后产生的文件(.class文件),只面向虚拟机。
采用字节码的好处:
- 在一定程度上解决了传统解释型语言执行效率低的问题,又保留了解释型语言可移植的特点。
- 字节码不针对一种特定的机器,因此,Java程序无需重新编译就可以在多种不同的计算机上运行
扩展:Java中编译器和解释器执行流程
Java源代码→编译器→JVM可执行的Java字节码→JVM→JVM解释器→机器可执行的二进制机器码
Java和C++的区别
- 都是面向对象的语言,都支持封装、继承和多态
- Java不提供指针来直接访问内存,程序内存更加安全
- Java的类是单继承的,C++支持多继承;Java的接口可以多继承
- Java有自动内存管理机制,不需要程序员手动释放无用内存
2、Java基础语法
2.1 数据类型
- 基本数据类型
- 整型:byte、short、int、long
- 浮点型:float、double
- 字符型:char
- 布尔型:boolean
- 引用数据类型
- 类(Class)
- 接口
- 数组
包装类型
基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。
Integer x = 2; // 装箱 调用了 Integer.valueOf(2)
int y = x; // 拆箱 调用了 X.intValue()
2.2 访问修饰符
java中通过访问修饰符来保护对类、变量、方法和构造方法的访问,支持4种访问权限:
private:对同个类下可见,使用对象:变量、方法,不能修饰类
default:在一个包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法
protected:对同一个包内和所有子类可见,使用对象:变量、方法,不能修饰类
public:对所有类可见,使用对象:类、接口、变量、方法
2.3 常用关键字
this、super
-
this:指向对象本身的一个指针,形参与成员变量名称相同时,用this来区分。
-
super:指向父类对象的一个指针
final、finally、finalize
- final:修饰的类不可以被继承、修饰的方法不可以被重写、修饰的变量不可以被改写
- finally:一般在try-catch代码块中,在处理异常时一般吧一定要执行的代码块放在finally代码块中,表示不管是否出现异常,该代码块都会被执行,一般用来存放一些关闭资源的代码
- finalize:Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。
static
-
修饰成员变量、成员方法,不需要创建对象实例,被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享,可以通过类名.方法名调用
- 静态变量:被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时初始化
- 静态方法:静态方法在类加载时就存在,不依赖实例,所以静态方法必须有实现,不能是抽象方法
-
静态代码块,优化代码性能,在类初次加载的时候,会按照static的顺序来执行每个static块,并且只会执行一次,可以用来执行初始化操作
public class A { static { System.out.println("123"); } public static void main(String[] args) { A a1 = new A(); A a2 = new A(); } }
-
修饰类,只能修饰内部类,也就是静态内部类,静态内部类不能访问外部类的非静态的变量和方法。
-
静态导包:在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低
import static com.xxx.ClassName.*
注意事项:
-
静态只能访问静态
- 由于静态方法可以不通过实例对象进行调用,因此在静态方法中,不能调用非静态方法,也不可以访问非静态成员变量
-
非静态既可以访问非静态,也可以访问静态
-
初始化顺序:静态变量和静态语句块优先于实例变量和普通语句块
存在继承的情况下,初始化顺序为: - 父类(静态变量、静态语句块) - 子类(静态变量、静态语句块) - 父类(实例变量、普通语句块) - 父类(构造函数) - 子类(实例变量、普通语句块) - 子类(构造函数)
2.4 流程控制语句
// 判断语句
if...else;
// 循环控制语句
for(;;){}
while(){}
switch不支持long类型。
break、continue、return
- break:结束当前循环体
- continue:跳出本次循环,继续本次循环体的下一次循环
- return:结束当前方法,直接返回
3、面向对象
- 面向过程:具体化的、流程化的,一步步实现的过程,性能比面向对象高
- 面向对象:模型化,有封装、继承、多态的特性,可以设计出低耦合高内聚的系统,易维护、易复用、易扩展的优点
面向对象的特性
封装、继承、多态
- 封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式,提高代码的复用性和安全性
- 继承:使用已存在的类的定义作为建立新类的基础,子类可以增加新的数据或新的功能或重写父类功能,也可以使用父类的功能
- 多态:父类或接口定义的引用变量可以指向子类或接口实现类的实例对象,提高程序的拓展性。
- 实现多态的必要条件:继承、重写、向上转型
- 编译时多态:方法的重载
- 运行时多态:通过动态绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变。
重写和重载区别:
重载实现编译时的多样性,重写是实现运行时的多样性。
- 重写:发生在运行期,子类继承父类方法,重写方法功能,实现程序扩展。(方法名、形参相同)
- 重载:发生在编译期,在同一个类中,方法名相同,但参数列表不同的方法
面向对象使用场景:
面向对象的五大原则
- 单一职责原则:类的功能要单一,不要杂
- 开放封闭原则:对拓展开放,对修改封闭
- 里氏替换原则:子类可以替换父类在父类可以出现的任何地方
- 依赖倒置原则:高层次的模块不应该依赖低层次的模块,例如:具体实现应该依赖于抽象
- 接口分离原则:设计时采用多个特定功能的接口比采用一个通用接口好
抽象类和接口
- 抽象类:含有至少一个抽象方法的类,拥有构造方法和非抽象方法,不能实例化,需要具体实现类,抽象类是对类的抽象,是一种模板设计
- 接口:没有构造方法,[在java1.8后引入默认方法和静态方法],接口中除了static、final变量,不能有其他变量,接口方法默认修饰符是public,不能实例化,需要具体实现类,接口是行为的抽象,是一种行为的规范
成员变量和局部变量
- 成员变量:方法外部,类内部定义的变量,作用域在整个类有效,存储在堆内存中,随对象创建而存在、消失而消失,有默认的初始值
- 局部变量:类的方法内部,作用域在方法或语句体内,存储在栈中,随方法(语句体)的执行存在,当方法或语句体执行完,就自动释放,没有默认值
使用原则
- 就近原则,首先在局部范围找,有就使用,接着在成员位置找
构造方法
主要作用是完成对类对象的初始化工作。在没有声明构造方法的情况下,会有一个默认的无参构造方法,程序可以正常执行
构造方法的特性:
- 名字和类名相同
- 没有返回值,不能用void声明构造函数
- 生成类对象时自动执行,无需调用
静态方法和实例方法
在外部调用静态方法时可以使用"类名.方法名",也可以使用"对象名.方法名",但实例方法只能使用后者进行调用
静态方法只能访问静态成员变量和静态方法,而实例方法没有限制
内部类
成员内部类、局部内部类、匿名内部类、静态内部类
值传递和引用传递
- 值传递:在方法调用时,传递的参数是按值的拷贝传递
- 引用传递:在方法调用时,传递的参数是按引用的地址进行传递,也就是变量所对应的内存空间的地址。传递的值前后都指向同一个内存空间。
注:无论是基本类型和是引用类型,在实参传入形参时,都是值传递,也就是说传递的都是一个副本,而不是内容本身。
自动装箱和拆箱
-
装箱:将基本类型用对应的引用类型包装起来
-
拆箱:将引用类型转换为基本数据类型
原始类型: boolean,char,byte,short,int,long,float,double
包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
Java中的异常处理
在Java中,所有的异常都继承Throwable类,Throwable有两个重要子类Exception和Error。
Error:是程序无法处理的错误,表示运行应用程序中较严重问题。
Exception:是程序本身可以处理的异常。
Throwable常用方法:
public string getMessage() 返回异常发生的简要描述
public string toString() 返回异常发生的详细信息
public string getLocalizeMessage() 返回异常对象的本地化信息,需要重新该方法
public void printStackTrace() 在控制台打印Throwable对象封装的异常信息
异常处理:捕获异常 try{}catch{}finally{}
try块:用于捕获异常
catch:用于处理try捕获的异常
finally:无论是否捕获或处理异常,finally块里的语句都会被执行。
4、IO流
IO流是数据传输的通道。流是一组有序的数据序列
- 按照流的流向分,分为输入流和输出流
- 按照操作单元划分,分为字节流和字符流
- 按照流的角色划分为节点流和处理流
4.1 IO流分类
Java 的 I/O 大概可以分成以下几类:
- 磁盘操作:File
- 字节操作:InputStream 和 OutputStream
- 字符操作:Reader 和 Writer
- 对象操作:Serializable
- 网络操作:Socket
- 新的输入/输出:NIO
(1)根据操作的数据类型的不同可以分为 :字节流与字符流。
-
InputStream/Reader:输入流的基类,InputStream是字节输入流,Reader是字符输入流
-
OutputStream/Writer:输出流的基类,OutputStream是字节输出流,Writer是字符输出流
(2)根据数据的流向分为:输入流与输出流
4.2 Java I/O使用的设计模式
Java I/O使用装饰者模式实现,以 InputStream 为例,
- InputStream 是抽象组件;
- FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作;
- FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
4.3 BIO、NIO、AIO区别
-
BIO:Blocking I/O,同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。
- 优点就是代码比较简单、直观;
- 缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
-
NIO:New I/O,一种同步非阻塞I/O模型,在java1.4引入NIO框架,通过SocketChannel和ServerSocketChannel两种不同套接字通道实现,都支持阻塞和非阻塞两种模式
-
AIO:Asynchronous I/O,AIO是NIO的升级,是异步非阻塞的I/O模型。异步I/O是基于事件和回调机制实现的,也就是应用操作后直接返回,不会阻塞,当后台处理时,操作系统会通知相应的线程进行后续的操作
select、poll、epoll的区别
IO 多路复用实际上就是通过一种机制,一个进程可以监视多个fd
,一旦某个fd
就绪(一般是读就绪或者写就绪),能够通知程序进行相应的操作,这种机制目前有 select
、poll
、epoll
类别 | select | poll | epoll |
---|---|---|---|
支持的最大连接数 | 由FD_SETSIZE 限制 | 基于链表存储,没有限制 | 受系统最大句柄数限制 |
fd 剧增的影响 | 线性扫描fd 导致性能很低 | 同 select | 基于fd 上 callback 实现,没有性能下降的问题 |
消息传递机制 | 内核需要将消息传递到用户空间,需要内核拷贝 | 同 select | epoll 通过内核与用户空间共享内存来实现 |
fd:文件描述符,Linux 下系统各组件都是以文件描述符的形式存在的
参考:Java BIO与NIO
Java序列化
序列化是一种用来处理对象流的机制,将对象的内容进行流化
序列化实现:将需要被序列化的类继承Serialize接口,标注对象可被序列化
Files常用的方法
exists();
create();
creteDirectory()
delect();
实例
// 字节输入流
public static void main(String[] args) throws IOException {
File file = new File("D:\\workspace\\IDEA\\lessonDemo\\a.txt");
FileInputStream inputStream = new FileInputStream(file);
byte[] bytes = new byte[1024];
int len = 0;
while((len = inputStream.read(bytes)) != -1){
System.out.println(new String(bytes,0,len));
}
inputStream.close();
}
// 字节输出流
public static void main(String[] args) throws IOException {
File file = new File("D:\\workspace\\IDEA\\lessonDemo\\a.txt");
FileOutputStream output = new FileOutputStream(file);
byte[] bytes = "Java数据交流管道——IO流".getBytes();
output.write(bytes);
output.close();
}
5、反射
在运行状态中,动态获取任意一个类的信息以及动态调用对象的功能,称为java的反射机制。
- 优点:运行期类型判断,动态加载类,提高代码的灵活度;屏蔽实现的细节,方便用户使用
- 缺点:反射相当于一系列解释操作,性能比直接的java代码要差的多
反射应用场景
- 模块化开发,通过反射去调用对应的字节码
- 动态代理模式使用反射机制
- 框架:JDBC、Spring、SpringMVC
获取反射的方式
- 通过new对象实现反射机制
- 通过路径实现反射机制
- 通过类名实现反射机制
// 方式1 通过new对象获取
Student stu = new Studennt();
Class obj1 = stu.getClass();
// 2.通过相对路径
Class obj2 = Class.forName("fanshe.Student");
// 3.通过类名
Class obj3 = Student.class;
6、常用类
6.1 数组
Java数组是存储数据长度固定的容器,保证多个数据的数据类型一致。
数组特点
- 数组初始化完成,长度不可以改变
- 既可以存储基本类型,也可以存储引用类型的数据
- 数组本身是一种引用类型
数组在内存中的存储
数组常见异常:
- 数组越界异常
- 空指针异常
常用的方法
// 创建数组
int[] arr = new int[]{1,2,3,...}; // 静态初始化,初始化初始值
int[] arr = {1,2,3,...}
int[] arr = new int[100]; // 动态初始化,指定数组长度,默认初始值
arr.length(); // 获取数组的长度
// 功能类Arrays(里面的方法是静态方法)
Arrays.sort(int[] arr); // 对数组arr进行排序
Arrays.copyOf(arr,newlength); //复制数组arr,并指定新数组的长度
Arrays.copyRange(arr,fromIndex,toIndex);//指定复制的新数组的区域
Arrays.binarySearch(arr,key); // 查找数组中的key值,如果存在则返回该值的索引,否则返回-1(-6)
Arrays.binarySearch(arr,fromIndex,toIndex,key);//在指定的区域查询可以存在
Arrays.fill(int[] arr, int num); //填充,将num值填充进数组arr
Arrays.asList(int[] arr); // 将数组转为list列表
Arrays.toString(int[] arr); // 将数组转为字符串
6.2 String
String 被声明为 final,因此它不可被继承。(Integer 等包装类也不能被继承)
Spring特性
- 不变性,String是只读字符串,对它的任何操作,其本质都是新建一个对象,在把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性
- 常量池优化,String对象创建后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会自己返回缓存的引用
- final类型,表示String类不能被继承,提高系统的安全性
String、StringBuffer、StringBuilder
- 可变性:String是不可变的,StringBuffer、StringBuilder是可变的
- 线程安全性:String中的对象是不可变的、线程安全的;StringBuffer对方法加入了同步锁synchronized 或对调用的方法加了同步锁,线程安全;StringBuilder,线程不安全
- 性能:每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象。StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
使用总结:
- 如果操作少量的数据使用:String
- 单线程操作字符串缓冲区下操作大量数据:StringBuilder
- 多线程操作字符串缓冲区下操作大量数据:StringBuffer
String Pool
字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中。
当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。
下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得同一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2); // false
String s3 = s1.intern();
String s4 = s1.intern();
System.out.println(s3 == s4); // true
如果是采用 “bbb” 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。
String s5 = "bbb";
String s6 = "bbb";
System.out.println(s5 == s6); // true
在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。
常用方法
String s = "xuzy"; // 创建字符串对象s
s.length(); // 获取字符串长度
s.indexOf("substr"); // 指定字符串首次出现位置
s.lastindexOf("substr"); // 指定字符串最后出现位置
s.charAt(int index); // 获取指定索引位置的字符
s.substring(int beginIndex);
s.substring(int beginIndex,int endIndex); // 截取指定位置的字符串
s.getBytes(); // 返回字符串的byte类型数组
s.trim(); // 去除空格
s.replace(char old,char new); // 替换字符
s.equal(String str); // 判断字符串s和str是否相等
s.equalIsIgnoreCase(String str); // 忽略大小写
s.toLowerCase(); // 将s全部变为小写
s.toUpperCase();
s.split(String sign); // 以sign为分割字符,对字符串进行拆分
7、集合Collection
用于存储数据的容器。
7.1 常用集合类型
Collection集合
-
List:有序、可重复、元素有索引的集合。常用实现类:
- ArrayList:底层数据结构为Object数组,线程不安全
- LinkedList:底层数据结构为双向链表,线程不安全,LinkedList 还可以用作栈、队列和双向队列。
- Vector:底层数据结构为Object数组,线程安全
-
Set:无序、不重复、元素具有唯一性的容器。常用实现类:
- HashSet:底层基于HashMap实现,使用HashMap的Key保存元素,无序,唯一,线程不安全
- LinkedHashSet:LinkedHashSet继承HashSet,并且内部使用双向链表维护元素的插入顺序,其内部通过LinkedHashMap实现的。
- TreeSet:底层数据结构为红黑树,有序,唯一
-
Queue
- LinkedList:可以用它来实现双向队列
- PriorityQueue:基于堆结构实现,可以用它来实现优先队列
Map集合
Map集合是一个键值对集合,存储键、值之间的映射关系。key无序、唯一,value可重复。常用实现类:
- HashMap:JDK1.8前,由数组+链表组成,数组是HashMap主题,链表是为了解决hash冲突的。JDK1.8后,由数组+链表和红黑树组成,在解决hash冲突有了较大变化,在链表长度大于一个阈值(默认8),但数组长度小于64时会首先进行扩容,否则将会把链表转化为红黑树,以减少搜索时间。
- HashTable:数组+链表组成,线程安全
- TreeMap:基于红黑树实现(自平衡的排序二叉树)
- LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
- ConcurrentHashMap:采用**"分段锁"策略**,ConcurrentHashMap的主干是个Segment数组。Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。
7.2 ArrayList工作原理
ArrayList是基于数组实现的,支持快速访问。数组默认大小为10.
private static final int DEFAULT_CAPACITY = 10;
7.2.1 扩容:
添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1)
,也就是旧容量的 1.5 倍。
扩容操作需要调用 Arrays.copyOf()
把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
7.2.2 快速失败Fail-Fast
使用modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。代码参考上节序列化中的 writeObject() 方法。
7.2.3 并发环境下使用ArrayList方案
-
使用Collection.synchroizedList(); 得到一个线程安全的ArrayList
List<String> list = new ArrayList<>(); List<String> synList = Collections.synchronizedList(list);
-
使用concurrent并发包下的CopyOnWriteArrayList类
List<String> list = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList类简述:
1.读写分离
写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
写操作需要加锁,防止并发写入时导致写入数据丢失。
写操作结束之后需要把原始数组指向新的复制数组。
2.使用场景
CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。
3.缺点
- 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
- 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。
7.3 HashMap工作原理
(1)HashMap的数据结构
HashMap底层实现数据结构为数组+链表的形式,JDK8及其以后的版本中使用了数组+链表+红黑树实现,解决了链表太长导致的查询速度变慢的问题。
(2)HashMap基于Hash算法实现;
-
利用key的hashCode重新hash计算当前对象的元素在数组中的下标;
-
存储时如果出现hash冲突时,判断1)key相同,覆盖value,2)key不相同,则将当前的key-value放入链表中。
-
获取时,直接找到键对应hash值对应的下标,再根据equals方法在对应的链表(或红黑树)找到对应value。
(3)JDK1.7与JDK1.8区别
(4)HashMap扩容操作(JDK1.7)
HashMap里面默认的负载因子大小为0.75,也就是说,当Map中的元素个数(包括数组,链表和红黑树中)超过了16*0.75=12之后开始扩容。将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。
在多线程环境下,HashMap扩容可能会导致死循环。
HashMap的初始容量,加载因子,扩容增量是多少
HashMap的初始容量16,加载因子为0.75,扩容增量是原容量的1倍。HashMap扩容是指元素个数 (包括数组和链表+红黑树中)超过了16*0.75=12之后开始扩容。
解决Hash冲突的方法
-
拉链法 (HashMap使用的方法)
拉链法
的实现比较简单,将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 -
线性探测再散列法
-
二次探测再散列法
-
伪随机探测再散列法
适合作为HashMap的键的类
String和Interger这样的包装类很适合做为HashMap的键,因为他们是final类型的类,而且重写了equals和hashCode方法,避免了键值对改写,有效提高HashMap性能。
为了计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashCode的话,那么就不能从HashMap中找到你想要的对象。
一致性Hash算法:
7.4 ConcurrentHashMap
JDK1.7,ConcurrentHashMap采用分段锁策略,由Segment数组结构和HashEntry数组结构组成,Segment实现ReentrantLock,所以Segment是一种可重入锁,扮演锁的角色。HashEntry用于存储键值对数据。
JDK1.8,ConcurrentHashMap取消了Segment分段锁,采用CAS和Synchronized来保证并发安全,在CAS操作失败时使用内置锁synchronized,数据结构和HashMap1.8相似,由数组+链表/红黑树。Synchronized只锁定当前链表或红黑树的首节点,这样只要hash不冲突,就不会产生并发。
参考:ConcurrentHashMap 的工作原理及代码实现
7.5 各种集合比较
集合与数组的区别
-
数组是固定长度的;集合是可变长度的。
-
数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用类型。
-
数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。
ArrayList和LinkedList的区别
-
数据结构:ArrayList是动态数组的数据结构实现,LinkedList是双向链表的数据结构。
-
随机访问速率:ArrayList比LinkedList的随机访问效率高,ArrayList可以通过元素的序号快速获取元素对象。
-
增加和删除效率:在非首尾的增加或删除的操作,LinkedList比ArrayList效率高。
-
内存空间占用:LinkedList比ArrayList更占内存。因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
-
线程安全:均是线程不安全的。
List和Set区别
-
List是一个有序、元素可重复容器,Set是无序、元素不可重复的的。
-
List查找元素效率高、插入删除元素效率低,Set查找效率低、插入删除元素效率高。
HashSet和TreeSet区别
- **HashSet底层使用了HashMap实现。**保证元素唯一性的原理:判断元素的hashCode值是否相同。如果相同,还会继续判断元素的equals方法,是否为true
- **TreeSet底层使用了红黑树来实现。**保证元素唯一性是通过Comparable或者Comparator接口实现,具有排序功能
HashMap和HashTable区别
-
HashMap没有考虑同步,是线程不安全的;Hashtable使用了synchronized关键字,是线程安全的;
-
HashMap允许null作为Key;Hashtable不允许null作为Key,Hashtable的value也不可以为null
-
底层数据结构:在JDK1.8以后HashMap在解决hash冲突有了较大变化,当链表的长度大于阈值时,将链表转化为红黑树,以减少搜索时间。HashTable没有这样的机制。
扩展:
- HashMap线程不安全主要是考虑到了多线程环境下进行扩容可能会出现HashMap死循环
- Hashtable线程安全是由于其内部实现在put和remove等方法上使用synchronized进行了同步,所以对单个方法的使用是线程安全的。但是对多个方法进行复合操作时,线程安全性无法保证。 比如一个线程在进行get操作,一个线程在进行remove操作,往往会导致下标越界等异常。
Java集合中的快速失败(fast-fail)机制
快速失败是Java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast。
假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就可能会抛出并发修改异常(ConcurrentModificationException)异常,从而产生fast-fail快速失败。
快速失败机制底层实现:
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedModCount值,是的话就返回遍历;否则抛出异常,终止遍历。
ConcurrentHashMap和HashTable区别
- ConcurrentHashMap锁的方式是分段锁,而HashTable是使用synchronized锁,每次同步执行都要锁住整个结构。
- 在实际开发中,多线程环境下一般使用ConcurrentHashMap
扩展:ConcurrentHashMap分段锁实现方式
- 使用静态内部类MapEntry,封装映射表的键值对
- 使用静态内部类Segment继承ReentrantLock,是可重入锁ReentrantLock,每个Segment守护一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment锁
8、函数式接口
函数式接口:只有一个方法的接口
8.1 四大函数式接口
Function函数式接口
Function 函数型接口, 有一个输入参数,有一个输出
@FunctionalInterface
public interface Function<T, R> { // T参数类型,R返回值类型
R apply(T var1);
}
Function<String,String> function = (str)->{return str;};
Predicate断定型接口
Predicate断定型接口有一个输入参数,返回值只能是 布尔值
@FunctionalInterface
public interface Predicate<T> {
boolean test(T var1);
}
Predicate<String> predicate = (str)->{return str.isEmpty();};
Consumer消费型接口
Consumer消费型接口:只有输入,没有返回值
@FunctionalInterface
public interface Consumer<T> {
void accept(T var1);
}
Consumer<String> consumer = (str)->{System.out.println(str);};
Supplier供给型接口
Supplier供给型接口:没有参数,只有返回值
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Supplier supplier = ()->{return 1024;};
8.2 Lamda编程
Lambda表达式都是函数式接口的实现,函数式接口的定义是只有一个抽象方法的接口是一个函数式接口,通常这样的函数上面都有@FunctionalInterface的注解修饰
//基本语法
(parameters) -> expression 或 (parameters) ->{ statements; }
9、Stream流式计算
10、ForkJoin
ForkJoin在JDK1.7后出现,并行执行任务,提高效率。处理大数据量
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pkCYx6Nm-1601776599217)(C:\Users\徐梓泳\AppData\Roaming\Typora\typora-user-images\image-20200716143043808.png)]
ForkJoin特点
工作窃取
双端队列
ForkJoin 实现方法
Class ForkJoinTask
- RecursiveAction:子类,递归事件,没有返回值
- RecursiveTask:子类,递归任务,有返回值