面向对象编程三大特性
封装
在面对对象程式设计方法中,封装(英语:Encapsulation)是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。
封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。要访问该类的代码和数据,必须通过严格的接口控制。
封装最主要的功能在于我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段。适当的封装可以让程式码更容易理解与维护,也加强了程式码的安全性。
封装的优点:
-
良好的封装能够减少耦合
-
类内部的结构可以自由修改
-
可以对成员变量进行更精确的控制
-
隐藏信息,实现细节
继承
在Java中,继承是一种面向对象编程的核心概念,它允许一个类(子类或派生类)从另一个类(父类或基类)继承属性和方法。这意味着子类可以重用父类的代码,同时可以添加自己的属性和方法,从而促进了代码重用和扩展。以下是关于Java继承的一些关键概念和用法:
- 继承关系
在Java中,使用关键字 extends 来创建一个类的子类。子类继承了父类的所有非私有成员(字段和方法),包括构造方法。
一个父类可以有多个子类,但Java中不支持多重继承,即一个类不能直接继承多个父类。
- 超类和子类
超类是被继承的类,也成为父类或基类。
子类是继承超类的类,也称为派生类。
- 构造方法
子类可以调用父类的构造方法,以便在子类的构造方法中执行父类的初始化操作,这可以使用super关键字实现。
如果子类没有显式调用父类构造方法,则会隐式调用父类的无参数构造方法(如果存在的话)。
- 方法重写
子类可以重写父类的方法,从而提供自己的实现。重写方法必须具有相同的方法签名(名称、参数类型和返回类型)。
用 @Override 注解可以帮助编译器检查是否正确地重写了父类的方法。
class Parent {
void print() {
System.out.println("This is the parent class.");
}
}
class Child extends Parent {
@Override
void print() {
System.out.println("This is the child class.");
}
}
- super关键字
super 关键字用于在子类中访问父类的成员,可以用来调用父类的构造方法、父类的属性或调用父类的方法。
通过 super 可以避免成员名称冲突,明确指定是要访问父类的成员。
class Parent {
int x = 10;
}
class Child extends Parent {
int x = 20;
void print() {
System.out.println(x); // 20,子类的属性
System.out.println(super.x); // 10,父类的属性
}
}
- 多态性
继承也与多态性相关,允许将子类的对象赋给父类的引用变量,然后通过这些引用变量调用相应的方法。
Parent obj = new Child(); // 多态性
obj.print(); // 调用子类的print方法
继承是Java中实现代码重用、分层结构和多态性的关键机制之一。通过合理地使用继承,可以构建更有组织和可维护的代码。然而,应该小心使用继承,以避免过度复杂的继承层次和紧密耦合的类。在设计时要考虑到类之间的关系,以确保代码的可扩展性和可维护性。
多态
多态的优点:
- 消除类型之间的耦合关系
- 可替换性
- 可扩充性
- 接口性
- 灵活性
- 简化性
多态存在的三个必要条件:
- 继承
- 重写
- 父类引用指向子类对象
Parent p = new Child();
反射机制
反射机制有什么用?
-
通过java语言中的反射机制可以操作字节码文件。
-
通过反射机制可以操作代码片段。
反射机制的相关类在那个包下?
- java.lang.reflect.*;
反射机制相关的类有哪些?
-
java.lang.Class 代表字节码文件
-
java.lang.reflect.Method 代表字节码中的方法字节码
-
java.lang.reflect.Constructor 代表字节码中的构造方法字节码
-
java.lang.reflect.Field 代表字节码中的属性字节码
获取字节码的三种方式
-
第一种:Class c = Class.forName(“完整类名带包名”);
-
第二种:Class c = 对象.getClass();
-
第三种:java语言中任何一种类型,包括基本数据类型它都有class属性
抽象类
特点:
-
抽象类中可以构造方法
-
抽象类中可以存在普通属性、方法、静态属性和方法
-
抽象类中可以存在抽象方法
-
如果一个类中有一个抽象方法,那么当前类一定是抽象类;抽象类中不一定有抽象方法
-
抽象类中的抽象方法,需要有子类实现,如果子类不实现,则子类也需要定义为抽象的
接口
特点:
-
在接口中只有方法的声明,没有方法体
-
在接口中只有常量,因为定义的变量,在编译的时候都会默认加上public static final
-
在接口中的方法,永远都被public来修饰
-
接口中没有构造方法,也不能实例化接口的对象
-
接口可以实现多继承
-
接口中定义的方法都需要有实现类来实现,如果实现类不实现接口中的所有方法则实现类定义为抽象类
迭代器
Java迭代器(Iterator)是一种用于遍历集合类(Collection)的对象的机制。它提供了一种统一的方式来访问不同类型的集合,例如列表(List)、集合(Set)和映射(Map),而无需了解底层数据结构或实现细节。迭代器通常用于循环遍历集合中的元素,适用于各种情况。
以下是关于Java迭代器的基本概念和用法:
- 获取迭代器:要获取一个集合的迭代器,通常使用集合对象的 iterator() 方法。例如,对于一个ArrayList,可以使用 iterator() 方法来获取一个迭代器。
ArrayList<String> list = new ArrayList<>();
Iterator<String> iterator = list.iterator();
- 迭代元素:一旦获得了迭代器,可以使用它来遍历集合中的元素。常见的迭代方法包括 hasNext() 和 next()。
while (iterator.hasNext()) {
String element = iterator.next();
// 处理元素
}
- 删除元素(可选):有些迭代器支持从集合中删除元素。这通常使用迭代器的 remove() 方法来实现。但并非所有集合都支持元素删除操作,如果不支持,调用 remove() 会抛出 UnsupportedOperationException 异常。
iterator.remove(); // 删除当前迭代的元素
- 增强的 for 循环:Java也提供了增强的 for 循环(for-each),它可以简化集合的遍历操作,但底层仍然使用了迭代器。
for (String element : list) {
// 处理元素
}
异常(Exception)
非检查性异常:
检查性异常:
异常方法:
java内存模型、内存结构
内存结构
jvm中的堆、栈、方法区等等,是java虚拟机的内存结构,java程序启动后,会初始化这些内存的数据。内存结构就是上图中内存空间这些东西,而java内存模型,完全是另外的一个东西。
内存模型
在多CPU的系统中,每个CPU都有多级缓存,一般分为L1、L2、L3缓存,因为这些缓存的存在,提供了数据的访问性能,也减轻了数据总线上传输的压力,同时也带来了很多新的挑战,比如两个CPU同时去操作同一个内存地址,会发生什么?在什么条件下,它们可以看到相同的结果?这些都是需要解决的。
所以在CPU层面,内存模型定义了一个充分必要条件,保证其它CPU的写入动作对该CPU是可见的,而且该CPU的写入动作对其它CPU也是可见的,那这种可见性应该如何实现呢?
有些处理器提供了强内存模型,所以CPU在任何时候都能看到内存中任意位置相同的值,这种完全是硬件提供的支持。
其它处理器提供了弱内存模型,需要执行一些特殊指令(就是经常看到或者听到的,memory barriers内存屏障),刷新CPU缓存的数据到内存中,保证这个写操作能够被其它CPU可见,或者将CPU缓存的数据设置为无效状态,保证其它CPU的写操作对本CPU可见。通常这些内存屏障的行为由底层实现,对于上层语言的程序员来说是透明的(不需要太关心内存屏障是如何实现的)。
内存屏障除了实现CPU的数据可见性之外,还有一个重要职责,可以禁止指令重排序。这里说的重排序可以发生在好几个地方:编译器、运行时、JIT等,比如编译器会觉得把一个变量的写操作放在最后会更有效率,编译后,这个指令就在最后了(前提是只要不改变程序的语义,编译器、执行器就可以这样自由的随意变化),一旦编译器对某个变量的写操作进行优化(放到最后),那么在执行之前,另一个线程将不会看到这个执行结果。
当然了,写动作可能被移到后面,那也可能被挪到了前面,这样的“优化”有什么影响呢?在这种情况下,其它线程可能会在程序实现“发生”之前,看到这个写入动作(这里怎么理解,指令已经执行了,但是在代码层面还没执行到)。通过内存屏障的功能,我们可以禁止一些不必要、或者会带来负面影响的重排序优化,在内存模型的范围内,实现更高的性能,同时保证程序的正确性。
java集合
集合和数组的区别
Collection集合的方法
常用集合分类
-
Collection接口的接口对象集合(单列集合)
- List接口:元素按进入先后有序保存,可重复
- LinkedList接口实现类,链表,插入删除,没有同步,线程不安全
- ArrayList接口实现类,数组,随机访问,没有同步,线程不安全
- Vector接口实现类,数组,同步,线程安全
- Stack是Vector类的实现类
- Set接口:仅接收一次,不可重复,并做内部排序
- HashSet使用hash表(数组)存储元素
- LinkHashSet链表维护元素的插入次序
- TreeSet底层实现为二叉树,元素排好序
- HashSet使用hash表(数组)存储元素
- List接口:元素按进入先后有序保存,可重复
-
Map接口,键值对的集合(双列集合)
- Hashtable接口实现类,同步,线程安全
- HashMap接口实现类,没有同步,线程不安全
- LinkedHashMap双向链表和哈希表实现
- WeakHashMap
-
currentHashMap
-
TreeMap红黑树对所有的key进行排序
-
IdentifyHashMap
List和Set集合详解
Set具有与Collection完全一样的接口,因此没有任何额外的功能,不像前面有两个不同的List。实际上Set就是Collection,只是行为不同(这是继承与多态思想的典型应用:表现不同的行为)。Set不保存重复的元素,Set存入Set的每个元素都必须是唯一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set与Collection有完全一样的接口。Set接口不保证维护元素的次序。
List和Set的区别
List
ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素。
LinkedList:底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素。
Vector:底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素。
Set
HashSet底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素,元素的唯一性是靠所存储元素类型是否重写hashCode()和equals()方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性。
具体实现唯一性的比较过程:存储元素首先会使用hash()算法函数生成一个int类型hashCode散列值,然后已经的所存储的元素的hashCode值比较,如果hashCode不相等,则所存储的两个对象一定不相等,此时存储当前的新的hashCode值处的对象;如果hashCode相等,存储元素的对象还是不一定相等,此时会调用equals()方法判断两个对象的内容是否相等,如果内容相等,那么就是同一个对象,无需存储;如果比较的内容不相等,那么就是不同的对象,就该存储了,此时就要采用哈希的解决地址冲突算法,在当前hashCode值处类似一个新的链表,在同一个hashCode值的后面存储不同的对象,这样就保证了元素的唯一性。
Set的实现类的集合对象中不能够有重复元素,HashSet也一样,它是使用了一种标识来确定元素的不重复,HashSet用一种算法来保证HashSet中的元素是不重复的,HashSet采用哈希算法,底层用数组存储数据。默认初始化容量16,加载因子0.75。
Object类中的hashCode()的方法是所有子类都会继承这个方法,这个方法会用Hash算法算出一个Hash(哈希)码值返回,HashSet会用Hash码值去和数组长度取模,模(这个模就是对象要存放在数组中的位置)相同时才会判断数组中的元素和要加入的对象的内容是否相同,如果不同才会添加进去。
覆盖hashCode()方法的原则:
1、一定要让那些我们认为相同的对象返回相同的hashCode值
2、尽量让那些我们认为不同的对象返回不同的hashCode值,否则就会增加冲突的概率。
3、尽量的让hashCode值散列开(两值用异或运算可使结果的范围更广)
HashSet的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成,我们应该为保存到HashSet中的对象覆盖hashCode()和equals(),因为再将对象加入到HashSet中时,会首先调用hashCode方法计算出对象的hash值,接着根据此hash值调用HashMap中的hash方法,得到的值&(length-1),得到该对象在hashMap的transient Entry[] table中的保存位置的索引,接着找到数组中该索引位置保存的对象,并调用equals方法比较这两个对象是否相等,如果相等则不添加。注意:所以要存入HashSet的集合对象中的自定义类必须覆盖hashCode()、equals()两个方法,才能保证集合中元素不重复。在覆盖equals()和hashCode()方法时,要使相同对象的hashCode()方法返回相同值,覆盖equals()方法再判断其内容。为了保证效率,所以在覆盖hashCode()方法时,也要尽量使不同对象尽量返回不同的Hash码值。
如果数组中的元素和要加入的对象的hashCode()返回了相同的Hash值(相同对象),才会用equals()方法来判断两个对象的内容是否相同。
LinkedHashSet底层数据结构采用链表和哈希表共同实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。线程不安全,效率高。
TreeSet底层数据结构采用二叉树来实现,元素唯一且已经排好序;唯一性同样需要重写hashCode()和equals()方法,二叉树结构保证了元素的有序性。根据构造方法不同,分为自然排序(无参构造)和比较器排序(有参构造),自然排序要求元素必须实现Compareable接口,并重写里面的compareTo()方法,元素通过比较返回的int值来判断排序序列,返回0说明两个对象相同,不需要存储;比较器需要在TreeSet初始化的时候传入一个实现Comparator接口的比较器对象,或者采用匿名内部类的方式new一个Comparator对象,重写里面的compare()方法。
List、Set和Vector总结
-
List、Set都是继承自Collection接口,Map则不是。
-
List特点:元素有放入顺序,元素可重复。Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉。(注意:set元素虽然无放入顺序,但是元素在Set中的位置是由该元素的HashCode决定的,其位置其实是固定的,加入Set的Object必须定义equals()方法,另外list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为它无序,无法用下标来取得想要的值。)
-
Set和list对比:
-
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
-
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其它元素位置改变。
-
-
ArrayList与LinkedList的区别和适用场景
-
ArrayList:
-
优点:ArrayList是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。
-
缺点:因为地址连续,ArrayList要移动数据,所以插入和删除操作效率比较低。
-
-
LinkedList:
-
优点:基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址,对于新增(add)和删除(remove)操作,LinkedList比较占优势。LinkedList适用于要头尾操作或插入指定位置的场景。
-
缺点:因为LinkedList要移动指针,所以查询操作性能比较低。
-
-
-
Vector和ArrayList都是用数组实现的,主要有这么三个区别:
-
Vector是多线程安全的,线程安全就是说多线程访问同一代码,不会产生不确定的结果。而ArrayList不是,这个可以从源码中看出,Vector类中的方法很多有synchronized进行修饰,这样就导致了vector在效率上无法与ArrayList相比;
-
两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的增加方式是不同。
-
Vector可以设置增长因子,而ArrayList不可以。
-
Vector是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。
-
Map详解
Map用于保存具有映射关系的数据,Map里保存这两组数据:key和value,它们都可以是任何引用类型的数据,但key不能重复。所以通过指定的key就可以取出对应的value。
(1) 请注意!!!Map没有继承Collection接口,Map提供key到value的映射,你可以通过“键”查找“值”。一个Map中不能包含相同的key,每个key只能映射一个value。Map接口提供三种集合的视图,Map的内容可以被当作一组key集合,一组value集合,或者一组key-value映射。
(2) Map:
(3) HashMap和HashTable的比较
(4) TreeMap:
(5) Map的其它类 IdentityHashMap和HashMap的具体区别,IdentityHashMap使用==判断两个key是否相等,而HashMap使用的是equals方法比较key值。有什么区别呢?
对于==,如果作用于基于基本数据类型的变量,则直接比较其储存的“值”是否相等;如果作用于引用类型的变量,则比较的是所指向的对象的地址。
对于equals方法,注意:equals方法不能作用于基本数据类型的变量。
如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容。
小结
HashMap非线程安全
HashMap:基于哈希表实现。使用HashMap要求添加的键类明确定义了hashCode()和equals(),为了优化HashMap空间的使用,可以调优初始容量和负载因子。
TreeMap:非线程安全基于红黑树实现。TreeMap没有调优选项,因为该树总处于平衡状态。
适用场景分析:
HashMap和HashTable:HashMap去掉了HashTable的contains方法,但是加上了containsValues()和containsKey()方法。HashTable同步的,而HashMap是非同步的,效率上比HashTable要高。HashMap允许空键值,而HashTable不允许。
HashMap:适用于Map中插入、删除和定位元素。
TreeMap:适用于按自然顺序或自定义顺序遍历键(key)。
线程是否安全:
LinkedList、ArrayList、HashSet、是非线程安全的,Vector是线程安全的;
HashMap是非线程安全的,HashTable是线程安全的;
StringBuilder是非线程安全的,StringBuffer是线程安全的。
数据结构:
ArrayXXX:底层数据结构是数组,查询快,增删慢
LinkedXXX:底层数据结构是链表,查询慢,增删快
HashXXX:底层数据结构是哈希表。依赖两个方法:hashCode()和equals()
TreeXXX:底层数据结构是二叉树。两种方式排序:自然排序和比较器排序
序列化
序列化的含义、意义及使用场景
序列化:将对象写入到IO流中
反序列化:从IO流中恢复对象
意义:序列化机制允许将实现序列化的Java对象转换字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。
使用场景:所有可在网络上传输的对象都必须是可序列化的,比如RMI(remote method invoke,即远程方法调用),传入的参数或返回的对象都是可序列化的,否则会出错;所有需要保存到磁盘的java对象都必须是可序列化的。通常建议:程序创建的每个JavaBean类都实现Serializeable接口。
序列化的实现方式
如果需要将某个对象保存到磁盘上或者通过网络传输,那么这个类应该实现Serializable接口或者Externalizable接口之一。
Serializabel
1. 普通序列化
Serializable接口是一个标记接口,不用实现任何方法。一旦实现了此接口,该类的对象就是可序列化的。
序列化步骤:
1) 创建一个ObjectOutputStream输出流;
2) 调用ObjectOutputStream对象的writeObject输出可序列化对象。
反序列化步骤:
1) 创建一个ObjectInputStream输入流;
2) 调用ObjectInputStream对象的readObject()得到序列化的对象。
同一对象序列化多次,会将这个对象序列化多次吗?答案是否定的。
- java序列化算法:
- 所有保存到磁盘的对象都有一个序列化编码号
- 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。
- 如果此对象已经序列化过,则直接输出编号即可。
Java序列化算法潜在的问题:
由于java序列化算法不会重复序列化同一个对象,只会记录已序列化对象的编号。如果序列化一个可变对象(对象内的内容可更改)后,更改了对象的内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存序列化编号。
可选的自定义序列化:
有些时候,我们需要某些属性不需要序列化。使用transient关键字选择不需要序列化的字段。
从输出我们看到,使用transient修饰的属性,Java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。
使用transient虽然简单,但将此属性完全隔离在了序列化之外。java提供了可选的自定义序列化。可以进行控制序列化的方式,或者对序列化数据进行编码加密等。
Externalizable--强制自定义序列化
通过实现Externalizabel接口,必须实现writeExternal、readExternal方法。
注意:Externalizabel接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;特别之处是必须提供public的无参构造器,因为在反序列化的时候需要反射创建对象。
两种序列化对比
| 实现Serializable接口 | 实现Externalizable接口 |
|---|---|
| 系统自动存储必要的信息 | 程序员决定存储哪些信息 |
| Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持 | 必须实现接口内的两个方法 |
| 性能略差 | 性能略好 |
虽然Externalizabel接口带来了一定的性能提升,但复杂度也提高了,所以一般通过实现Serializable接口进行序列化。
序列化版本号serialVersionUID
我们知道,反序列化必须拥有class文件,但随着项目的升级,class文件也会升级,序列化怎么保证升级前后的兼容性呢?
java序列化提供了一个private static final long serialVersionUID的序列号版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。
如果反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常。
序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;不指定版本号另一个明显隐患是不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。
什么情况下需要修改serialVersionUID呢?分三种情况。
-
如果只是修改了方法,反序列化不容影响,则无需修改反序列化;
-
如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;
-
如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。
总结
- 所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。
- 对象的类名、实例变量(包括基本类型、数组、对其它对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
- 如果想让某个变量不被序列化,使用transient修饰。
- 序列化对象的引用类型成员变量,也必须是可序列化的,否则会报错。
- 反序列化时必须有序列化对象的class文件。
- 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
- 单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。
- 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不重复序列化。
- 建议所有可序列化的类加上serialVersionUID版本号,方便项目升级。
java IO字节流和字符流常见类总结
数据的传输都是通过两种类型的流:输入流和输出流,这就是IO。
流的继承关系图:
需要读入数据使用输入流,需要写入数据使用输出流;
按照操作的数据类型分类:字节流和字符流
字节流可以读取和写入任何数据,因为任何数据最终都能以字节存储;
字符流只能操作文本类型的文件,按照字符进行读取和写入,方便对字符的操作
常用的一些字节流子类:
-
文件输入输出流:FileInputStream、FileOutputStream
-
对象输入输出流:ObjectInputStream、ObjectOutputStream
常用的一些字符流子类:
-
文件输入输出流:FileReader FileWriter
-
缓存的文件输入输出流:BufferedReader BufferedWriter
String、StringBuffer、StringBuilder之间区别
简要的说,String类型和StringBuffer类型的主要性能区别其实在于String是不可变的对象,因此在每次对String类型进行改变的时候其实都等同于生成了一个新的String对象,然后将指针指向新的String对象,这样不仅效率低下,而且大量浪费有限的内存空间,所以经常改变内容的字符串最好不要用String。因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后,JVM的GC就会开始工作,那速度是一定会相当慢的。
StringBuffer字符串变量(线程安全)
StringBuilder字符串变量(非线程安全)
当对字符串进行修改的时候,特别是字符串对象经常改变的情况下,需要使用StringBUffer和StringBuilder类。和String类不同的是,StringBuffer和StringBuilder类的对象能够被多次的修改,并且不产生新的未使用对象。
StringBuilder类在Java5中被提出,它和StringBuffer之间最大不同在于StringBuilder的方法不是线程安全的(不能同步访问)。由于StringBuilder相较于StringBuffer有速度优势,所以多数情况下建议使用StringBuilder类。然而在应用程序要求线程安全的情况下,则必须使用StringBuffer类。
小结
- 如果要操作少量的数据用String;
- 多线程操作字符串缓冲区下操作大量数据StringBuffer;
- 单线程操作字符串缓冲区下操作大量数据StringBuilder;
Object类
java中有一个比较特殊的类,就是Object类,它是所有类的父类,如果一个类没有使用extends关键字明确标识继承另外一个类,那么这个类就默认继承Object类。因此,Object类是Java类层中的最高层类,是所有类的超类。换句话说,Java中人格一个类都是它的子类。由于所有类都是由Object类衍生出来的,所以Object类中的方法适用于所有类。
Object类有12个成员方法,按照用途可以分为以下几种:
- 构造函数
- hashCode和equals函数用来判断对象是否相同
- wait()、wait(long)、wait(long, int)、notify()、notifyAll()
- toString()和getClass
- clone()
- finalize()用于在垃圾回收
CurrentHashMap
哈希表
哈希表就是一种以 键-值(key-indexed) 存储数据的结构,我们只要输入待查找的值即key,即可查找到其对应的值。
哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。
链式哈希表
链式哈希表从根本上说是由一组链表构成。每个链表都可以看做是一个“桶”,我们将所有的元素通过散列的方式放到具体的不同的桶中。插入元素时,首先将其键传入一个哈希函数(该过程称为哈希键),函数通过散列的方式告知元素属于哪个“桶”,然后在相应的链表头插入元素。查找或删除元素时,用同们的方式先找到元素的“桶”,然后遍历相应的链表,直到发现我们想要的元素。因为每个“桶”都是一个链表,所以链式哈希表并不限制包含元素的个数。然而,如果表变得太大,它的性能将会降低。
应用场景
我们熟知的缓存技术(比如redis、memcached)的核心其实就是在内存中维护一张巨大的哈希表,还有大家熟知的HashMap、CurrentHashMap等的应用。
ConcurrentHashMap与HashMap、HashTable的区别
HashMap
我们知道HashMap是线程不安全的,在多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。
HashTable
HashTable和HashMap的实现原理几乎一样,差别无非是
- HashTable不允许key和value为null
- HashTable是线程安全的
但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁。
多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。
ConcurrentHashMap
主要就是为了应对hashmap在并发环境下不安全而诞生的,ConcurrentHashMap的设计与实现非常精巧,大量的利用了volatile,final,CAS等lock-free技术来减少锁竞争对于性能的影响。
我们都知道Map一般都是数组+链表结构(JDK1.8该为数组+红黑树)。
ConcurrentHashMap避免了对全局加锁改成了局部加锁操作,这样就极大地提高了并发环境下的操作速度,由于ConcurrentHashMap在JDK1.7和1.8中的实现非常不同,接下来我们谈谈JDK在1.7和1.8中的区别。
JDK1.7版本的CurrentHashMap的实现原理
在JDK1.7中ConcurrentHashMap采用了数组+Segment+分段锁的方式实现。
Segment(分段锁)
ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
内部结构
ConcurrentHashMap使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。如下图是ConcurrentHashMap的内部结构图:
从上面的结构我们可以了解到,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作。
第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。
该结构的优劣势
坏处
这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长(两次Hash)
好处
写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上)。
所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高。
JDK1.8版本的CurrentHashMap的实现原理
JDK8中ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作,这里我简要介绍下CAS。
CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
JDK8中彻底放弃了Segment转而采用的是Node,其设计思想也不再是JDK1.7中的分段锁思想。
Node:保存(final)key,(volatile)value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性。
Java8 ConcurrentHashMap结构基本上和Java8的HashMap一样,不过保证线程安全性。
在Java8中,ConcurrentHashMap弃用了Segment类,但是保留了Segment属性,用于序列化。目前ConcurrentHashMap采用Node类作为基本的存储单元,每个键值对(key-value)都存储在一个Node中。同时Node也有一些子类,TreeNodes用于树结构中(当链表长度大于8时转化为红黑树);TreeBins用于维护TreeNodes。当链表转树时,用于封装TreeNode。也就是说,ConcurrentHashMap的红黑树存放的是TreeBin,而不是treeNode;ForwordingNodes是一个重要的结构,它用于ConcurrentHashMap扩容时,是一个标志节点,内部有一个指向nextTable的属性,同时也提供了查找的方法(find方法用于查找元素);
处理Node之外,Node的一个子类ForwardingNodes也是一个重要的结构,它主要作为一个标记,在处理并发时起着关键作用,有了ForwardingNodes,也是ConcurrentHashMap有了分段的特性,提高了并发效率。
ConcurrentHashMap中的原子操作
在ConcurrentHashMap中通过原子操作查找元素、替换元素和设置元素。这些原子操作起着非常关键的作用,你可以在所有ConcurrentHashMap的基本功能中看到它们的身影。
ConcurrentHashMap的功能实现
ConcurrentHashMap初始化
在介绍初始化之前先介绍一个重要的参数sizeCtl,含义如下:
这个参数起到一个控制标志的作用,在ConcurrentHashMap初始化和扩容都有用到。 ConcurrentHashMap构造函数只是设置了一些参数,并没有对Hash表进行初始化。当在从插入元素时,才会初始化Hash表。在开始初始化的时候,首先判断sizeCtl的值,如果sizeCtl < 0,说明有线程在初始化,当前线程便放弃初始化操作。否则,将SIZECTL设置为-1,Hash表进行初始化。初始化成功以后,将sizeCtl的值设置为当前的容量值。
确定元素在Hash表的索引
通过hash算法可以将元素分散到哈希桶中。在ConcurrentHashMap中通过如下方法确定数组索引: 第一步:
第二步:(length-1) & (h ^ (h >>> 16)) & HASH_BITS);
ConcurrentHashMap的put方法
- 如果key或者value为null,则抛出空指针异常;
- 如果table为null或者table的长度为0,则初始化table,调用initTable()方法。
- 计算当前键值的索引位置,如果Hash表中当前节点为null,则将元素直接插入。(注意,这里使用的就是前面锁说的CAS操作)
- 如果当前位置的节点元素的hash值为-1,说明这是一个ForwaringNodes节点,即正在进行扩容。那么当前线程加入扩容。
- 当前节点不为null,对当前节点加锁(synchronized),将元素插入到当前节点。在Java8中,当节点长度大于8时,就将节点转为树的结构。
ConcurrentHashMap的扩容机制
当ConcurrentHashMap中元素的数量达到cap * loadFactor时,就需要进行扩容。扩容主要通过transfer()方法进行,当有线程进行put操作时,如果正在进行扩容,可以通过helpTransfer()方法加入扩容。也就是说,ConcurrentHashMap支持多线程扩容,多个线程处理不同的节点。
- 开始扩容,首先计算步长,也就是每个线程分配到的扩容的节点数(默认是16)。这个值是根据当前容量和CPU的数量来计算(stride = (NCPU > 1) ? (n >>> 3) / NCPU : n),最小是16。
- 接下来初始化临时的Hash表nextTable,如果nextTable为null,初始化nextTable长度为原来的2倍;
- 通过计算出的步长开始遍历Hash表,其中坐标是通过一个原子操作(compareAndSetInt)记录。通过一个while循环,如果在一个线程的步长内便跳过此节点。否则转下一步;
- 如果当前节点为空,之间将此节点在旧的Hash表中设置为一个ForwardingNodes节点,表示这个节点已经被处理过了。
- 如果当前节点元素的hash值为MOVED(f.hash == -1),表示这是一个ForwardingNodes节点,则直接跳过。否则,开始重新处理节点;
- 对当前节点进行加锁,在这一步的扩容操作中,重新计算元素位置的操作与HashMap中是一样的,即当前元素键值的hash与长度进行&操作,如果结果为0则保持位置不变,为1位置就是i+n。其中进行处理的元素是最后一个符合条件的元素,所以扩容后可能是一种倒序,但在Hash表中这种顺序也没有太大的影响。
- 最后如果是链表结构直接获得高位与低位的新链表节点,如果是树结构,同样计算高位与低位的节点,但是需要根据节点的长度进行判断是否需要转化为树的结构。
ConcurrentHashMap的get方法
ConcurrentHashMap的get方法就是从Hash表中读取数据,而且与扩容不冲突。该方法没有同步锁。
- 通过键值的hash计算索引位置,如果满足条件,直接返回对应的值;
- 如果相应节点的hash值小于0 ,即该节点在进行扩容,直接在调用ForwardingNodes节点的find方法进行查找。
- 否则,遍历当前节点直到找到对应的元素。
在JDK8中ConcurrentHashMap的结构,由于引入了红黑树,使得ConcurrentHashMap的实现非常复杂,我们都知道,红黑树是一种性能非常好的二叉查找树,其查找性能为O(logN),但是其实现过程也非常复杂,而且可读性也非常差,Doug
Lea的思维能力确实不是一般人能比的,早期完全采用链表结构时Map的查找时间复杂度为O(N),JDK8中ConcurrentHashMap在链表的长度大于某个阈值的时候会将链表转换成红黑树进一步提高其查找性能。
总结
其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树。
1.数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
2.保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。
3.锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。
4.链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
5.查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。
ArrayList详解
ArrayList概述
ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存。
ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类。
ArrayList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问,实现了Cloneable接口,能被克隆。
每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。
注意,此实现不是同步的。如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。
ArrayList的实现
对于ArrayList而言,它实现List接口、底层使用数组保存所有元素。其操作基本上是对数组的操作。下面我们来分析ArrayList的源代码:
私有属性
ArrayList定义只定义类两个私有属性:
很容易理解,elementData存储ArrayList内的元素,size表示它包含的元素的数量。
有个关键字需要解释:transient。
Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。
有点抽象,
被标记为transient的属性在对象被序列化的时候不会被保存。
接着回到ArrayList的分析中......
构造方法
ArrayList提供了三种方式的构造器,可以构造一个默认初始容量为10的空列表、构造一个指定初始容量的空列表以及构造一个包含指定collection的元素的列表,这些元素按照该collection的迭代器返回它们的顺序排列的。
元素存储
ArrayList提供了set(int index, E element)、add(E e)、add(int index, E element)、addAll(Collection c)、addAll(int index, Collection c)这些添加元素的方法。下面我们一一讲解:
// 用指定的元素替代此列表中指定位置上的元素,并返回以前位于该位置上的元素。
public E set(int index, E element) {
RangeCheck(index);
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
// 将指定的元素添加到此列表的尾部。
public boolean add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
return true;
}
// 将指定的元素插入此列表中的指定位置。
// 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加1)。
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);
// 如果数组长度不足,将进行扩容。
ensureCapacity(size+1); // Increments modCount!!
// 将 elementData中从Index位置开始、长度为size-index的元素,
// 拷贝到从下标为index+1位置开始的新的elementData数组中。
// 即将当前位于该位置的元素以及所有后续元素右移一个位置。
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
// 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部。
public boolean addAll(Collection c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
// 从指定的位置开始,将指定collection中的所有元素插入到此列表中。
public boolean addAll(int index, Collection c) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
ArrayList是基于数组实现的,属性中也看到了数组,具体是怎么实现的呢?比如就这个添加元素的方法,如果数组大,则在将某个位置的值设置为指定元素即可,如果数组容量不够了呢?
看到add(E e)中先调用了ensureCapacity(size+1)方法,之后将元素的索引赋给elementData[size],而后size自增。例如初次添加时,size为0,add将elementData[0]赋值为e,然后size设置为1(类似执行以下两条语句elementData[0]=e;size=1)。将元素的索引赋给elementData[size]不是会出现数组越界的情况吗?这里关键就在ensureCapacity(size+1)中了。
元素读取
元素删除
ArrayList提供了根据下标或者指定对象两种方式的删除功能。如下:
romove(int index):
首先是检查范围,修改modCount,保留将要被移除的元素,将移除位置之后的元素向前挪动一个位置,将list末尾元素置空(null),返回被移除的元素。
remove(Object o)
首先通过代码可以看到,当移除成功后返回true,否则返回false。remove(Object o)中通过遍历element寻找是否存在传入对象,一旦找到就调用fastRemove移除对象。为什么找到了元素就知道了index,不通过remove(index)来移除元素呢?因为fastRemove跳过了判断边界的处理,因为找到元素就相当于确定了index不会超过边界,而且fastRemove并不返回被移除的元素。下面是fastRemove的代码,基本和remove(index)一致。
removeRange(int fromIndex,int toIndex)
执行过程是将elementData从toIndex位置开始的元素向前移动到fromIndex,然后将toIndex位置之后的元素全部置空顺便修改size。
这个方法是protected,及受保护的方法,为什么这个方法被定义为protected呢?
这是一个解释,但是可能不容易看明白。stackoverflow.com/questions/2…
先看下面这个例子
输出结果是[0, 1, 4, 5, 6],结果是不是像调用了removeRange(int fromIndex,int toIndex)!哈哈哈,就是这样的。但是为什么效果相同呢?是不是调用了removeRange(int fromIndex,int toIndex)呢?
调整数组容量ensureCapacity
从上面介绍的向ArrayList中存储元素的代码中,我们看到,每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过一个公开的方法ensureCapacity(int minCapacity)来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。
数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。
Object oldData[] = elementData;//为什么要用到oldData[]
乍一看来后面并没有用到关于oldData, 这句话显得多此一举!但是这是一个牵涉到内存管理的类, 所以要了解内部的问题。 而且为什么这一句还在if的内部,这跟elementData = Arrays.copyOf(elementData, newCapacity); 这句是有关系的,下面这句Arrays.copyOf的实现时新创建了newCapacity大小的内存,然后把老的elementData放入。好像也没有用到oldData,有什么问题呢。问题就在于旧的内存的引用是elementData, elementData指向了新的内存块,如果有一个局部变量oldData变量引用旧的内存块的话,在copy的过程中就会比较安全,因为这样证明这块老的内存依然有引用,分配内存的时候就不会被侵占掉,然后copy完成后这个局部变量的生命期也过去了,然后释放才是安全的。不然在copy的的时候万一新的内存或其他线程的分配内存侵占了这块老的内存,而copy还没有结束,这将是个严重的事情。
ArrayList和Vector区别
- ArrayList在内存不够时默认是扩展50% + 1个,Vector是默认扩展1倍。
- Vector提供indexOf(obj, start)接口,ArrayList没有。
- Vector属于线程安全级别的,但是大多数情况下不使用Vector,因为线程安全需要更大的系统开销。
ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过trimToSize方法来实现。
Fail-Fast机制:
ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。具体介绍请参考这篇文章深入Java集合学习系列:HashMap的实现原理 中的Fail-Fast机制。
Array和ArrayList之间的区别
- 简单理解
ArrayList是一种可以自动扩充的Array。
- Array类型的变量在声明的同时必须进行实例化(至少得初始化数组的大小),而ArrayList可以只是先声明。
- Array只能存储同构的对象,而ArrayList可以存储异构的对象。
如:声明为int[]的数组就只能存放整形数据,string[]只能存放字符型数据,但声明为object[]的数组除外。而ArrayList可以存放任何不同类型的数据,因为它里面存放的都是被装箱了的Object型对象,实际上ArrayList内部就是使用”object[] _items;”这样一个私有字段来封装对象的。
- Array是始终是连续存放的,而ArrayList的存放不一定连续。
- Array对象的初始化必须只定指定大小,且创建后的数组大小是固定的,
而ArrayList的大小可以动态指定,其大小可以在初始化时指定,也可以不指定,也就是说该对象的空间可以任意增加。
- Array不能够随意添加和删除其中的项,而ArrayList可以在任意位置插入和删除项。
总结
关于ArrayList的源码,给出几点比较重要的总结:
- 注意其三个不同的构造方法。无参构造方法构造的ArrayList的容量默认为10,带有Collection参数的构造方法,将Collection转化为数组赋给ArrayList的实现数组elementData。
- 注意扩充容量的方法ensureCapacity。ArrayList在每次增加元素(可能是1个,也可能是一组)时,都要调用该方法来确保足够的容量。当容量不足以容纳当前的元素个数时,就设置新的容量为旧的容量的1.5倍加1,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量),而后用Arrays.copyof()方法将元素拷贝到新的数组(详见下面的第3点)。从中可以看出,当容量不够时,每次增加元素,都要将原来的元素拷贝到一个新的数组中,非常之耗时,也因此建议在事先能确定元素数量的情况下,才使用ArrayList,否则建议使用LinkedList。
- ArrayList的实现中大量地调用了Arrays.copyof()和System.arraycopy()方法。我们有必要对这两个方法的实现做下深入的了解。
首先来看Arrays.copyof()方法。它有很多个重载的方法,但实现思路都是一样的,我们来看泛型版本的源码:
这里可以很明显地看出,该方法实际上是在其内部又创建了一个长度为newlength的数组,调用System.arraycopy()方法,将原来数组中的元素复制到了新的数组中。
下面来看System.arraycopy()方法。该方法被标记了native,调用了系统的C/C++代码,在JDK中是看不到的,但在openJDK中可以看到其源码。该函数实际上最终调用了C语言的memmove()函数,因此它可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多,很适合用来批量处理数组。Java强烈推荐在复制大量数组元素时用该方法,以取得更高的效率。
- ArrayList基于数组实现,可以通过下标索引直接查找到指定位置的元素,因此查找效率高,但每次插入或删除元素,就要大量地移动元素,插入删除元素的效率低。
- 在查找给定元素索引值等的方法中,源码都将该元素的值分为null和不为null两种情况处理,ArrayList中允许元素为null
Java的四种引用类型
①强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。
②软引用:简言之,如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中
③弱引用(这里讨论ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null
④虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象)