最近开始看Java guide的基础知识,作为查漏补缺使用,发现还是有听过概念之前没有搞德特别清楚的,下面做一些记录。
一些Java中的关键字
final
1、final修饰的变量是常量,如果是基本数据类型的变量,则其数值在初始化后就不可改变;如果是引用类型的变量,则对其初始化后就不能再执行其他对象(但是指向的对象本身是可变的)。
2、类中所有的private方法都隐式地指定为final。
static
1、对于静态代码块来说,它定义在类的方法之外,静态代码块最先执行(静态代码块—>非静态代码块—>构造函数)。这个类不论创建多少个对象,静态代码块只执行一次。一个类中的静态代码块可以有多个,它们都不在任何 方法体内,JVM在加载类时会执行这些静态代码块(按照它们在类中的出现顺序)。静态代码块对于定义在它之后的静态变量,可以赋值,但是不能访问。 类似这样:
static {
i = 4;
System.out.println(i); //error
}
private static int i;
静态代码块(static {})与非静态代码块({},也叫构造代码块):它们都是在JVM在加载类时且在构造方法之前执行,在类中都可以定义多个,按照定义的顺序执行,一般在代码块中对一些static变量进行赋值。 不同之处在于:静态代码块在非静态代码块之前执行;静态代码块只在第一次new时执行一次,而非静态代码块在每一次new都会执行一次。非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。
一般情况下,如果有些代码比如一些项目最常用的变量或对象必须在项目启动的时候就执行的时候,需要使用静态代码块,这种代码是主动执行的。比如监听某个配置的代码。
attention: 静态代码块不一定只是在第一次new时执行,这里第一次new本质上是初始化阶段,但是类初始化除了第一次new,还有其他可能性,比如通过 Class.forName(“ClassDemo”)创建 Class 对象的时候也会执行。但是呢,也不能简单认为JAVA静态代码块在类被加载时就会自动执行。我们后续会继续深入学习一个类的运行过程(装载、连接和初始化)。
这里还需要注意的是,非静态代码块和构造函数的区别:非静态代码块是负责给所有的对象进行统一初始化,而构造函数是给对应的对象初始化,因为不同的构造函数创建的对象是不一样的,但是无论创建哪个对象, 都会先执行相同的非静态代码块,也就是说,非静态代码块中定义的是不同对象的共性的初始化内容。
2、static修饰的成员变量存放在Java 内存区域的方法区。方法区和堆一样,也是多个线程共享的区域,它用于存储被虚拟机加载的类信息,常量,静态变量以及即时编译器(JIT)编译后的代码等。
3、静态内部类(static修饰类的话,只能修饰内部类)。静态内部类和非静态内部类的最大区别在于:非静态内部类在编译完成后会隐含保存一个引用,该引用指向创建它的外围类,但是静态内部类是没有的。也就是说,静态 内部类的创建不需要依赖外围类的创建;它也不能使用任何外围类的非static成员变量和方法。这个特点经常被我们用来实现单例模式,代码如下:
public class Singleton {
//声明为 private 避免调用默认构造方法创建对象
private Singleton() {
}
// 声明为 private 表明静态内部该类只能在该 Singleton 类中被访问
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getUniqueInstance() {
return SingletonHolder.INSTANCE;
}
}
当上面的Singleton类被JVM加载时,静态内部类SingletonHolder并没有被加载到内存(延迟加载),只有当调用getUniqueInstance()方法时,才会触发访问SingletonHolder.INSTANCE,这时SingletonHolder才 会被加载到JVM内存,此时初始化INSTANCE实例,并且只会被JVM初始化一次(这一点是由JVM保证的线程安全,即使有多个线程调用的情况下)。
反射
获取Class对象的四种方式
1、在知道具体类型时,可以使用TargetObject.class方式获取Class对象,注意“:通过这种方式获取Class对象不会针对这个类进行初始化,也就是不会执行这个类的static方法块。 这种方式适用于在编译时已经知道具体的类。
2、通过Class.forName(“cn.javaguide.TargetObject”),内部实际调用的是一个native方法 forName0(className, true, ClassLoader.getClassLoader(caller), caller),true表示类是否需要初始化, 默认是需要初始化的,也就是这种方法会执行这个类的static方法块。
3、通过这个类的实例对象的getClass()方法获取,显然这个方法首先需要一个当前类的对象。
4、通过类加载器xxxClassLoader.loadClass(“cn.javaguide.TargetObject”),通过类加载器获取Class对象不会进行初始化,意味着不进行包括初始化等一些列步骤,静态块和静态对象不会得到执行。
动态代理
基于反射可以在运行时创建接口的动态实现,这就是动态代理(核心在于在运行时创建接口的动态实现,而非编译时)。动态代理最常用的实现方式分别是JDK动态代理和CGLIB动态代理,对于JDK动态代理来说, 调用newProxyInstance()方法就可以创建动态代理,这个方法有三个参数:
InvocationHandler handler = new MyInvocationHandler();
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
MyInterface.class.getClassLoader(),
new Class[] { MyInterface.class },
handler);
第一个参数是一个类加载器,用于加载动态代理类;第二个参数是需要实现的接口数组;第三个参数是一个InvocationHandler对象,将代理上的所有方法调用都转发到InvocationHandler对象上,上面的代码执行完成以后, proxy就包含了MyInterface接口的动态实现,对代理的所有调用都将由到实现了InvocationHandler接口的handler对象来处理。
对动态代理的所有方法调用都转发到实现接口的InvocationHandler对象。 InvocationHandler接口只有一个方法invoke
public interface InvocationHandler{
Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
Object proxy:实现接口的动态代理对象,通常不需要;
Method method:表示在动态代理实现的接口上调用的方法,通过Method对象,可以获取到方法名,参数类型,返回类型等信息。
Object[] args:方法的参数值,注意:如果接口中的参数是int、long等基本数据时,这里的args必须使用Integer, Long等包装类型。
上面的JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。 它的第二个参数是接口数组,如果一个类没有实现某一个接口(比如它就是个简单的类,不需要太多复杂的抽象封装),那么就不能对这个类进行动态代理, 这里就需要使用到 CGLIB 动态代理机制。
CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB通过继承方式实现代理,一些知名的开源框架中都使用到了CGLIB,比如Spring中的AOP模块, 如果目标对象实现了接口,就默认采用JDK动态代理;否则采用CGLIB动态代理。
集合
HashMap
HashMap中没有capacity的概念,只有size和threshold,尤其需要注意 this.threshold = tableSizeFor(initialCapacity);这个方法。
HashMap可以存储null的key和value,但是null作为键只能有一个,null作为值可以有多个;HashTable不允许有null的键和值。
JDK 1.8以后HashMap在解决hash冲突时,当链表长度大于默认阈值(8)时,会将链表转换为红黑树(不过转换前会先判断数组桶的长度,如果小于64, 会首先进行扩容,而不是转换为红黑树)。HashMap的长度(数组长度)总是2的幂(方便计算槽位,不用求余,(n - 1) & hash”。(n 代表数组长度))。
TreeMap 和 HashMap 都继承自AbstractMap ,但是需要注意的是TreeMap它还实现了NavigableMap接口和SortedMap 接口:
-
1、实现NavigableMap接口让TreeMap有了对集合内元素的搜索的能力;
-
2、实现SortedMao接口让TreeMap可以对集合中的元素按照键排序,默认按照key的升序排序,也可以自定义比较器。而HashMap是按照键的hashcode值来 存储数据的;LinkedHashMap是按照插入顺序的;而TreeMap是按照键排序的。
在JDK1.8以前,多线程使用HashMap可能会产生循环链表问题,主要是在于并发的rehash造成的,即使后面解决了这个问题,多线程下使用HashMap还是 可能会有其他问题,不能使用。
JDK1.8中,HashMap添加一个元素的流程,即put过程:
-
(1)、首先判断数组table是否为空或者长度是否为0,如果是则先调用resize进行扩容,第一次put时会发生这种情况,因为HashMap初始化后,并没有 初始化相应的数组;
-
(2)、根据hash计算数组的下标,index是hash计算出来的数组下标,如果对应位置有值而且这个key存在,那么就直接覆盖然后返回。流程结束。
-
(3)、如果对应的位置没有值,直接插入即可,即table[index] = newNode(hash, key, value, null); 然后size++,并且判断自增后 的size是否大于扩容阈值threshold,如果大于则进行扩容。
-
(4)、如果对应的位置有值,但是key并不存在,证明我们要把这个数据通过拉链法来解决冲突了。那么需要判断table[index]是否为树节点,也就是第一个 节点的类型,如果是树节点,那么将新节点插入到红黑树中。然后size++,并且判断自增后的size是否大于扩容阈值threshold,如果大于则进行扩容。
-
(5)、接上面那一步,如果table[index]不是树节点,证明此时这个位置还是一个链表,那么将新节点插入链表(通过遍历放入尾部,尾插法 ),然后判断当前链表 长度是否大于8,如果小于8,则无须进行转换(红黑树),然后size++,判断是否需要进行扩容;如果大于8,则调用treeifyBin()方法转换为红黑树。
-
(6)、在treeifyBin()方法内部,还判断了数组的长度即 (n = tab.length) < MIN_TREEIFY_CAPACITY,如果小于64,那么是不会转化为红黑树的, 会进行扩容resize然后就返回了,如果大于64才进行转换。同样,后续如果由于删除或者其他原因调整了大小,当红黑树的节点小于或等于 6 个以后,又会恢复 为链表形态。事实上,不是元素小于6的时候一定会变成链表,只有resize的时候才会根据UNTREEIFY_THRESHOLD 进行转换。
如果 hashCode 分布良好,也就是 hash 计算的结果离散好的话,那么红黑树这种形式是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。 在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为 8 的时候,概率仅为 0.00000006。这是一个小于千万分之一的概率,通常我们 的 Map 里面是不会存储这么多的数据的,所以通常情况下,并不会发生从链表向红黑树的转换。 但是,HashMap 决定某一个元素落到哪一个桶里,是和这个对象的 hashCode 有关的,JDK 并不能阻止我们用户实现自己的哈希算法,如果我们故意把哈希 算法变得不均匀,比如重写hashCode 计算出来的值始终为 1,那么就很容易导致 HashMap 里的链表变得很长。
在1.7及以前,put操作是不太一样的。主要在于:如果定位到的数组位置没有元素 就直接插入(没有判断是否扩容操作);如果定位到的数组位置有 元素,遍历以这个元素为头结点的链表,依次和插入的key比较,如果key相同就直接覆盖,不同就采用头插法插入元素(即放入链表的头节点)。
HashSet
HashSet底层就是基于HashMap实现的,代码并不是很多。当把一个对象加入到HashSet时,会计算其hashcode,以此来判断这个对象加入的位置。这里 需要注意的是,我们往往说HashSet是无序的,这里的无序指的是遍历顺序和加入顺序是无关的,但是其内部元素存储还是有序的(按照对象的hashcode)。 如果没有相同的hashcode,则认为对象没有重复出现,如果有相同的hashcode,再次调用equals方法,来判断对象是否真的相同,相同就不会加入成功了。
类的hashcode方法,只有在hash表相关的数据结构中会使用到。
对于引用类型来说,==比较的是两个引用是否指向同一个对象地址;而equals如果该类没有重写(override),则还是对比的地址是否相等(因为 Object类的equals方法就是直接使用的==,),所以一般都会重写equals方法,来自定义判断两个对象是否相等的逻辑。
ConcurrentHashMap
JDK1.7的ConcurrentHashMap底层还是使用的分段数组+链表来实现的,1.8以后和HashMap一样,也升级到可能的红黑树了。为了实现线程安全,在1.7 的时候,ConcurrentHashMap(分段锁)对整个桶数组进行了分割分段(Segment),每一把锁只锁定容器中的一部分数据,多线程访问容器里不同段的数据, 就不会存在竞争了,这一点也就是常说的优于HashTable的地方。但是在1.8以后已经摒弃了段的概念, 而是直接用 Node 数组+链表/红黑树 的数 据结构来实现,并发控制使用 synchronized 和 CAS 来操作。
synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。
首先通过hash找到对应的链表后,查看是否是第一个object,如果是,直接用cas原则插入,无需加锁,如果不是链表第一个object,则直接用链表的第一 个object加锁,这里加的锁是synchronized,虽然效率不如reentrantlock,但是节约了空间,这里会一直用第一个object为锁,直到重新计算map大小, 比如扩容或者操作了第一个object为止。
为什么在Java8中该分段锁已被弃用呢?主要还是加入多个分段锁浪费内存空间;而且put操作放入同一个段的概率并不高。
关于ConcurrentHashMap,可以再多看几遍文章。
LinkedList
LinkedList是一个实现了List接口和Deque接口的双端链表, 它底层是链表结构,也就是可以比较高效地支持插入和删除元素(不过你要是指定插入 index,那还是会先从头节点访问到这个位置,也就是O(n)的时间复杂度);也实现了Deque接口,因此具有队列的性质,即支持addFirst、addLast等方法。 它的源码还是比较简单的,也容易看懂。