泛型

定义

1:泛型就是定义一种模板,例如ArrayList<T>,然后在代码中为用到的类创建对应的ArrayList<类型>,一个模板处理多种数据类型。

使用泛型

2:可以把ArrayList<Integer>向上转型为List<Integer>T不能变!),但不能把ArrayList<Integer>向上转型为ArrayList<Number>T不能变成父类)。ArrayList和ArrayList两者完全没有继承关系。

3:如果不定义泛型类型时,泛型类型实际上就是Object

4:泛型Array可接受定义类型的子类元素:

编写泛型

5:可以在接口中定义泛型类型,实现此接口的类必须实现正确的泛型类型:class Person implements Comparable<Person>{}

6:编写泛型类泛型接口:把类或接口里面的特定类型SomeType替换为T,把T作为一种数据类型 来使用,并把类或者接口申明<T>public class Pair<T> {}public interface abc<T>{}

7:泛型类型<T>不能用于静态方法:

普通的方法是通过类的实例来调用的,在创建对象时传入了T的具体类型:new ArrayList<String>(),也就是说对象已经知道这个时候类上面定义 的的具体类型了;而静态方法不需要对象实例来调用:Pair.create(...),所以也就不知道的具体类型,虚拟机不允许这种情况发生,所以编译的时候就报错了。静态方法由于随着类的加载而加载,不能访问类的泛型(因为在创建对象的时候才确定),因此必须定义自己的泛型类型,放在static后面,你可以理解为既然静态方法不知道Pair里面的具体类型,你就手动的告诉它具体的类。

8 : 多个泛型类型

把类里面的特定类型SomeType替换为T,K,把T,K作为一种数据类型 来使用,并把类申明<T,K>public class Pair<T,K> {},使用的时候,需要指出两种类型:Pair<String, Integer> p = new Pair<>("test", 123);

擦拭法

9:Java语言的泛型实现方式是擦拭法

Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。

10:Java泛型的局限:

11:不恰当的覆写方法

 

12:泛型继承

extend 通配符

使用Pair<? extends Number>使得方法接收所有泛型类型为NumberNumber子类的Pair类型,<? extends Number>的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型T的上界限定在Number了。类型擦除后T不再变成Object而是Number

方法参数签名int add(Pair<? extends Number> p)的方法可以接受Pair<Double>,Pair<Integer>类型的参数p,经过泛型擦拭后变成<Number>而不是Object,在方法内部p就被视为Pair<Number>,相当于自动向上转型,只能进行与Number类型相符合的操作,不能进行Double ,Integer特有操作,因为该方法既可以接受Pair<Double>类型的参数,也可以接受Pair<Integer>类型参数,在方法内部无法确定数据的具体类型,但他们都是Number或者Number的子类,将数据类型视为Number是安全的。方法参数签名<? extends Number>无法改变实际传入函数的参数的泛型类型。

方法返回值使用类似<? extends Number>通配符作为返回值泛型类型时表示:方法返回值泛型类型为NumberNumber子类。

在方法内部p只有只读属性:无法调用传入Number引用的方法(null除外),因为add方法既可以接受Pair<Double>,也可以接受Pair<Integer>,p中数据类型是不确定的,传入的Number及其子类应用很可能与数据不相匹配,所以对p是只读属性。

定义泛型类型Pair<T>的时候,也可以使用extends通配符来限定T的类型:public class Pair<T extends Number> { ... }泛型类型限定为Number以及Number的子类。

super 通配符

使用<? super Integer>通配符表示:

super不能用在class定义处,只能用在方法参数。

15: <? extends T>类型和<? super T>类型的区别在于:

16 :无限通配符

反射与泛型

部分反射API也是泛型

18:带泛型的数组

无法直接创建泛型数组,必须通过强制转型实现带泛型的数组:Pair<String>[] ps = (Pair<String>[]) new Pair[2];

数组实际上在运行期没有泛型,编译器可以强制检查变量ps,因为它的类型是泛型数组。但是,编译器不会检查变量arr=new SomeClass[2],因为它不是泛型数组。如果两个变量一个带泛型,一个不带泛型,都指向同一个数组,不带泛型数组可以存入任意类型数据,则带泛型数组读取时如果类型不匹配就会出现错误。所以,操作arr可能导致从ps获取元素时报错,所以必须扔掉arr的引用,匿名对象直接赋值。

带泛型的数组实际上是编译器的类型擦除:SomeClass<String>[] => SomeClass<Object>[]SomeClass[]Class相同。

集合

简介

Java 集合, 一个是 Collection接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。对于Collection 接口,下面又有三个主要的子接口:List、Set 和 Queue。

List

2:通过List接口提供的of()方法,根据给定元素快速创建ListList.of("apple", "pear", "banana");创建List是只读的。

3: Iterator对象知道如何遍历一个List,并且不同的List类型,返回的Iterator对象实现也是不同的,但总是具有最高的访问效率。比如get(int)方法只有ArrayList的实现是高效的,换成LinkedList后,索引越大,访问速度越慢,使用迭代器实现针对每种List的高效访问,for each循环就是使用迭代器实现。Java的for each循环本身就可以帮我们使用Iterator遍历。只要实现了Iterable接口的集合类都可以直接用for each循环来高效遍历.

只要实现了Iterable接口的集合类都可以直接用for each循环来遍历,原因就在于Iterable接口定义了一个Iterator<E> iterator()方法,强迫集合类必须返回一个Iterator实例。

4:与array间转换

equals方法

正确使用Listcontains()indexOf()这些方法,放入的实例必须正确覆写equals()方法

简化引用类型的比较,我们使用Objects.equals()静态方法:return Objects.equals(this.name, p.name) && this.age == p.age;

equals()方法的正确编写方法:

  1. 先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;
  2. instanceof判断传入的待比较的Object是不是当前类型
  3. 如果是,继续比较,否则,返回false;允许子类和自己比就用instanceof,不允许子类和自己比就用class比
  4. 对引用类型用Objects.equals()比较,对基本类型直接用==比较。
 ArrayListLinkedList
获取指定元素速度很快需要从头开始查找元素
添加元素到末尾速度很快速度很快
在指定位置添加/删除需要移动元素不需要移动元素
内存占用较大

通常情况下,优先使用ArrayList而不是LinkedList

Map

Map中不存在重复的key,因为放入相同的key,只会把原有的key-value对应的value给替换掉。

创建Map: Map.of()

遍历Map

遍历Map时,不可假设输出的key是有序的!Map存储的是key-value的映射关系,并且,它不保证顺序。在遍历的时候,遍历的顺序既不一定是put()时放入的key的顺序,也不一定是key的排序顺序。使用Map时,任何依赖顺序的逻辑都是不可靠的。

loadFactor是HashMap负载程度的一个度量,即HashMap持有的元素数量和HashMap大小的比值,loadFactor是为了让HashMap尽可能不满而存在的,理论上一次成功/不成功的查找耗时均为O(1+a),a为装载因子,加载因子越大,填满的元素越多,空间利用率越高,但冲突的机会加大了,查找时间复杂度上升。反之加载因子越小,填满的元素越少,冲突的机会减小,查找时间复杂度下降,但空间浪费多了。当HashMap中的元素数量大于capacity*loadFactor时,HashMap就要扩容,并进行重新计算元素存储位置。

同时是在实际中,HashMap碰撞与否,其实是与hashCode()的设计有很大关系,如果哈希函数得当,就可以使哈希地址尽可能的均匀分布在哈希地址空间上,从而减少冲突的产生,但一个良好的哈希函数的得来很大程度上取决于大量的实践,JDK设计者在平衡空间利用和性能方面给了一个比0.693更高的经验数字,此外各种情形下设置也不尽相同,比如ThreadLocalMap的装载因子为2/3。转载因子变大查询复杂度升高,但是占用的空间降低了,对于内存消耗频繁/GC频繁的应用来说,如果能接受hashmap的查询耗时损耗,将转载因子变大可能是非常值得的。

虽然红黑树有更好的查找效率O(log(N)),但是TreeNode的大小约为链表节点的两倍,在红黑树进行插入、删除等操作时为了平衡红黑树还要进行额外的操作,维护成本明显高于链表。所以只有在一个拉链已经拉了足够节点(默认为8)并且HashMap容量大于等于64的时候才会转为tree,否则进行扩容(容量较小发生大量冲突直接扩容,此时的空间复杂度可以忍受)。在理想情况下,使用随机的hashcode值,loadfactor为0.75情况,桶中的Node的分布频率服从参数为0.5的泊松分布:桶中出现8个数据的概率已经小于一千万分之一,该情形达到的概率的较小,并且当这个hash桶的节点因为移除或者扩容后resize数量变小(默认为6)的时候,我们会将树再转为拉链,可见引入红黑树冲突解决是作为极端情况下的兜底方案(比如哈希DoS攻击:RESTful兴起,数据传输使用Json,在收到Json字符串后进行jsonDecode操作,攻击者借由发送一条充满数千个变量的POST报文,所有变量的hashcode相同,在将数据存入HashMap时,某个哈希桶中有大量数据,导致哈希函数就会超载,仅是处理此单一请求也需要大量时间,n个数据插入复杂度O(N^2)),所以将阈值设为8,此时不会经常出现链表转红黑树的情况,而达到阈值的哈希桶也可以红黑树实现更快的查找,实现兜底。

hashCode=31*hashCode+element.hashCode(),选择值31是因为它是一个奇素数。如果它是偶数并且乘法的结果溢出,高位信息就会丢失,因为乘以2相当于左移。31一个很好的特性是乘法可以通过移位,并获得更好的性能减法来代替:`31*i==(i<<5)-i,现代虚拟机会自动进行这种优化。实验表明从超过50,000个英语单词计算哈希值时,使用常量31、33、37、39和41将在每种情况下产生少于7次的冲突,这可能是许多Java实现选择此类常量的原因。

hash=(h=key.hashCode())^(h>>>16),key的hash值高16位不变,低16位与高16位异或作为key的最终hash值,最后元素下标为:index=(n-1)&hash,(本质为除n取余,当且仅当n=2^k时成立),其中n=table.length。

2884823463jdfdjfgjdf

因为table的长度都是2的幂,因此如果对hashCode直接取余,index仅与hashCode的低n位有关,hashCode的高位都被与操作置为0了,这样做很容易产生碰撞。设计者权衡了speed,utility,andquality,通过将高16位与低16位异或来获得hsah值,使下标值同时用到高位与低位的信息,hashCode只要有一位发生改变,整个hash返回值就会改变,保证了hash值的随机性。设计者考虑到现在的hashCode分布的已经很不错了,而且当发生较大碰撞时也用树形存储降低了冲突。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。

容量变为以前的两倍。对于每个哈希桶,如果桶里面只有一个元素,直接重新计算下标newIndex=(e.hash&(newCap-1))放入新的哈希表;如果桶内为多个元素的链表或者为红黑树。无论是红黑树还是链表都是挨个计算`(e.hash&oldCap)==0(其中oldCap=2^k)是否成立,如果成立表明hash的第k+1个二进制为0,此时newIndex=(newCap-1)&hash的最高位也就是第k+1个二进制位为0,也是时newIndex=oldIndex,newIndex仍旧在原先的旧位置,如果hash的第k+1个二进制为1,则newIndex相较于oldIndex多出来第k+1位,也就是newIndex=2^k+oldIndex=oldCap+oldIndex。通过遍历桶中元素,将元素分为低位(oldIndex)、高位(oldCap+oldIndex)两个链表。如果桶里面原先装的是红黑树还要判断各自是否满足树化条件,如果满足还要转换为红黑树(红黑树转换为两个链表比较合理,如果两个子数据能转换成红黑树,则原先红黑树至少16个数据,这概率基本为0),分别存储完成扩容。扩容的时间复杂度为O(N),一次扩容后还能再插入N个数据,所以扩容的成本可以均摊到后续N个元素中,每个元素的扩容成本为O(1),最终元素的插入时间复杂度仍为O(1)。

HashMap使用延迟初始化,在第一次添加数据时才初始化数据结构,如果table没有使用过的情况则以默认大小进行一次resize。计算key的hashCode、hash、索引值,然后获取底层table数组的数据,如果获取的数据为null,表明不存在hash冲突,则直接插入;如果不为null先检查该bucket第一个元素是否是和插入的key相等,相等则直接替换;如果和第一个元素的key不相等并且是TreeNode的情况,调用TreeNode的put方法将数据插入红黑树,并进行相应的左旋、右旋等平衡操作;否则桶内就存储的是单链表,循环遍历链表,如果找到相等key的节点则跳出循环,替换节点的值,并将旧值返回;如果链表中没找到key相等的节点达到最后一个节点时将新的节点添加到链表最后,此时新增了key-value对,如果桶内元素个数达到树化的阈值就将单链表转换为红黑树。最后判断总的元素个数判断是否超过了threshold,如果超过则需要进行resize扩容,然后返回null。

初始化容量:initialCapacity的默认值是16,即使内存足够,也不能将initialCapacity设得过大,虽然大初始化容量可避免扩容导致的效率的下降,get和put方法都是常数复杂度的,也不是因此而增加时间复杂度。但是实际的程序可能不仅仅使用get和put方法,也有可能使用迭代器,如使用EntrySet迭代时,底层实现时挨个遍历哈希桶,再在桶里挨个遍历节点,如果initialCapacity容量较大,导致大量空哈希桶,那么会使迭代器效率降低。所以理想的情况还是在使用HashMap前估计一下数据量,太小反复扩容导致得数组复制、重新计算下标、重新构建红黑树的开销,太大空间利用率低,迭代器遍历成本上升。

key一般选择Immutable对象(Immutable:创建之后就不能发生改变,任何对它的改变都应该产生一个新的对象;对象应该是final的,以此来限制子类继承父类,以避免子类改变了父类的immutable特性;如果类中包含mutable类对象,那么返回给客户端的时候,返回该对象的一个拷贝,而不是该对象本身,防止用户修改。这也不绝对,只要参与计算hashCode、equals、compare得字段不变即可),并且覆写hashCode()以及equals()方法,如果在HashMap中使用可变对象作为Key带来的问题:如果HashMapKey发生变化,导致hashCode()/equal()的结果改变,那么该key在HashMap中的存取时可能再也查找不到这个Entry了。常见的Key比如String、Integer这些类已经很规范的覆写了hashCode()以及equals()方法,并且作为不可变类天生是线程安全的,可以不用被synchronize就在并发环境中共享,可以很好的优化比如因为不可变所以可以缓存hash值,避免重复计算等。

HashMap、HashTable、ConcurrentHashMap

1,线程安全

Hashtable、ConcurrentHashMap是线程安全,HashMap是非线程安全。多线程环境下HashTable的对数据进行修改的方法都使用了synchronized描述符,来满足线程安全的特性,使用了对象级别的同步锁,读和写操作都需要获取锁,本质上仍然只允许一个线程访问,其他线程被排斥在外,当每次对Map做修改操作的时候都会锁住这个Map对象。HashMap在多线程环境下也可以使用Collections.synchronizedMap()方法来获取一个线程安全的集合。Collections.synchronizedMap()实现原理是Collections定义了一个SynchronizedMap的内部类,这个类实现了Map接口,在调用方法时使用synchronized来保证线程同步,虽然synchronized不再放在方法上,而是放在方法内部,使用this作为互斥量作为同步块出现,但仍然是对象级别的同步锁,和hashTable没有太大区别。HashTable已经被淘汰了,如果不需要线程安全,那么使用HashMap,如果需要线程安全,那么使用ConcurrentHashMap。

ConcurrentHashMap底层使用Node(value 、next都用了volatile修饰,保证了可见性)数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作:写入数据时调用Unsafe的本地方法获取指定内存中的数据,保证每次拿到的数据都是最新的,定位出的节点如果为空表示当前位置可以写入数据,利用CAS尝试写入,失败则不断尝试保证成功,因为HashMap负载因子为0.75,平均下来每个桶最多只有0.75个元素,所以插入数据时桶内为空的概率仍然较大,此时使用CAS乐观锁写入数据,性能可以得到很大提升;如果获得的元素非空利用synchronized锁住桶中的第一个节点,这里是对数组元素加锁(Node),无论是相较于Collections.synchronizedXXX返回的包装类还是HashTable,加锁粒度更细,多个桶可以并发读写。

HashMap并发不安全:两个put的key发生了碰撞(hash值一样),那么根据HashMap的实现,这两个key会添加到数组的同一个位置,这样最终就会发生其中一个线程的put的数据被覆盖。此外如果多个线程同时检测到元素个数超过数组大小*loadFactor,会发生多个线程同时对hash数组进行扩容,可能会引起链表成环而导致死循环的错误。ConcurrentHashMap是并发安全的,插入数据时通过原子操作判断哈希桶下有没有其它线程对数据进行了修改,保证了同时只有一个线程修改链表,防止出现链表成环,然后开始写入数据;读取时按照链表或者红黑树正常读取即可(Node字段value、next都用了volatile修饰,保证了可见性)。

并发问题的三个来源:原子性、可见性、有序性。ConcurrentHashMap只能保证提供的原子性读写操作是线程安全的,也就是put()、get()操作是线程安全的。可见性问题: CPU 在计算时优先从离自己最近、速度最快的 CPU 缓存中获取数据去计算,其次再从内存中获取数据,导致数据不一致。原子性问题:比如注册用户,使用先判断是否存在,再写入数据,如果两个线程同时发现用户不存在,之后都进行写数据,将导致出现重复添加问题,可以两个操作放在一起执行完,这与数据库事务的原子性理解差不多。有序性:编译器为了提高性能有时候会改变代码执行的顺序,对于单线程代码指令重排序对于执行没有什么影响,但是会对多线程并发代码执行产生不可预知的结果。原理可以参考上节的原子性问题。提供的putIfAbsent接口,其含义是如果 key 已经存在则返回存储的对象,否则返回null,这个方法实现加了synchronized锁,为线程安全。在这个场景中如果不使用putIfAbsent就要对register(User user)方法加锁,对于性能的影响更大。

2,存储差异

HashTable底层实现是数组+单链表;HashMap是数组+单链表/红黑树;ConcurrentHashMap是数组+单链表/红黑树。HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。HashMap扩容时是capacity2,Hashtable扩容时是capacity2+1。HashTable会尽量使用素数、奇数。而HashMap则总是使用2的幂作为哈希表的大小。当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀,而HashMap使用对hashCode二次运算增强hash值得随机性,来弥补容量不是素数的缺点,同时将哈希表的大小固定为了2的幂可以用位运算来替代取余速度更快。

3,空值处理

HashMap支持null键和null值,而HashTable、ConcurrentHashMap在遇到key或者value为null时,会抛出NullPointerException异常。这仅仅是因为HashMap在实现时对null做了特殊处理,将null的hashCode值定为了0,从而将其存放在哈希表的第0个bucket中,因此在HashMap中不能由get()方法来判断HashMap中是否存在某个key,应该用containsKey()方法(containsKey,根据key获得节点,通过判断节点是否为null来判断key是否存在,即使key和value都为null的节点,节点本身也不为null)来判断。而concurrenthashmap它们是用于多线程的,并发的,如果map.get(key)得到了null,不能判断到底是映射的value是null,还是因为没有找到对应的key而为空,相较于单线程状态的hashmap却可以用containKey(key)去判断到底是否包含了这个null;支持并发的ConcurrentHashMap在调用containskey和m.get(key)时ConcurrentHashMap可能已经不同了。

4,迭代器实现

HashMap的迭代器是fail-fast(旨在停止正常运行,而不是尝试继续可能存在缺陷的过程)迭代器,而ConcurrentHashMap、HashTable的迭代器不是fail-fast的,因为要支持并发。所以当在使用迭代器遍历HashMap时数据结构上被修改(增加或者移除元素不包括更新节点值),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛ConcurrentModificationException异常。

ThreadLocal

1,ThreadLocal提供了线程局部(thread-local)变量。在多线程中为每一个线程创建单独的变量副本的类,在使用ThreadLocal类型变量进行相关操作时,都会通过当前线程获取到ThreadLocalMap来完成操作,每个线程的ThreadLocalMap是属于线程自己的,ThreadLocalMap中维护的值也是属于线程自己的,这就保证了ThreadLocal类型的变量在每个线程中是独立的,在多线程环境下不会相互影响。一个Thread派生一个ThreadLocalMap,该线程下的多个ThreadLocal可以向Map中存值,key为ThreadLocal本身。

2,使用场景:某个线程下的多个方法都需要使用数据库连接,如果在每次需要连接的地方都新建一个连接,使用完再关闭,是在高并发下导致服务器压力非常大,并且严重影响程序执行性能。使用ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。

3,根据当前线程获得类型为ThreadLocalMap的成员属性threadLocals,threadLocals类似于HashMap(未实现Map接口),给当前线程缓存了数据,要使用的时候就从本线程的threadLocals对象中获取就可以了,key就是当前Threadlocla对象,ThreadLocal使用两个静态变量保存前一个ThreadLocal的哈希值与哈希值的递增值,hashCode从0开始,每新建一个ThreadLocal,对应的hashcode就递增一个常量。ThreadLocalMap提供了基于ThreadLocal哈希值的get、set、remove等方法。查找时如果当前线程的threadLocals未初始化,则重新创建一个ThreadLocalMap对象,并且调用当前ThreadLocal的initialValue方法(默认返回null,可被重写)生成初始值并添加到ThreadLocalMap对象中并返回。如果threadLocals中不存在以当前ThreadLocal对象为Key的的对象,那么调用当前ThreadLocal的initialValue方法生成初始值,并且添加到当前线程的threadLocals中,并返回,如果存在就返回。

4,ThreadLocalMap用于维护线程本地变量值,是一个Map却没有实现Map接口,用一个Entry(弱引用)数组来存储Entry。数组中的Entry并不是链表形式,而是每个bucket里面仅仅放一个Entry,使用线性探测法解决冲突,因为ThreadLocalMap中存储元素并不多,转载因子(2/3)小,发生哈希冲突也不严重,直接使用线性探查法解决即可,没必要使用拉链法额外增加复杂度,此外数据全存储在数组中可以利用CPU缓存加快查询速度。如果插入数据时发生冲突则不断后移(到尾部后移到头图)至找到空位则插入或者指向对象key为null则运行过期元素清理,因为Entry的key使用的是弱引用的方式,如果ThreadLocal外部的不存在强应用,ThreadLocal将被回收,这时就无法再访问到key对应的value,需要把这样的无效Entry清除掉来腾出空间,并将元素插入。这里能通过线性探查法能解决冲突的前提数组有空位以供插入,这是通过在插入新元素导致数组中元素个数增加时,会进行过期元素清理与容量检查,空位不足则扩容,保证插入后数组中元素永远小于长度的2/3,永远有空位。查找时如果根据哈希值计算出的下标的Entry的key对不上,则后移遍历数组寻找key相等的元素返回,找不到返回null。

5,内存泄漏

equals()、hashCode()

7:HashMap之所以能根据key直接拿到value,原因是它内部通过空间换时间的方法,用一个大数组存储所有value,并根据key直接计算出value应该存储在哪个索引。

Map的内部,对key做比较是通过equals()实现的,通过key计算索引的方式就是调用key对象的hashCode()方法,它返回一个int整数。HashMap正是通过这个方法直接定位key对应的value的索引,继而直接返回value

两个实例ab

借助Objects.hash()来计算哈希值:return Objects.hash(name, age);``equals()用到的用于比较的每一个字段,都必须在hashCode()中用于计算;equals()中没有使用到的字段,绝不可放在hashCode()中计算。

 

实际上HashMap初始化时默认的数组大小只有16,可以通过哈希值取余数进行索引映射:int index = key.hashCode() & 0xf;,当数组不够用了,内部自动扩容为原来的两倍,需要重新确定hashCode()计算的索引位置:int index = key.hashCode() & 0x1f;由于扩容会导致重新分布已有的key-value,所以,频繁扩容对HashMap的性能影响很大。如果我们确定要使用一个容量为10000key-valueHashMap,更好的方式是创建HashMap时就指定容量:Map<String, Integer> map = new HashMap<>(16384);(HashMap内部的数组长度总是2n)

HashMap的数组中,实际存储的不是一个ValueClass实例,而是一个List,它包含两个Entry,一个是"keyObj"的映射,一个是"valueObj"的映射:List<Entry<KeyClass, ValueClass>>,HashMap内部通过"keyObj"的hash值找到的实际上是List<Entry<KeyClass, ValueClass>>,它还需要遍历这个List,并找到一个Entry,它的key字段是"keyObj",这里key的比较就是通过的equals(),所以要同时重写equals(),hashCode(),在key相同后才能返回对应的valueObj实例。这个List就越长,Mapget()方法效率就越低。

hashmap中依据key的hash值来确定value存储位置,所以一定要重写hashCode方法,而重写equals方法,是为了解决hash冲突,如果两个key的hash值相同,就会调用equals方法,比较key值是否相同,如果发生哈希冲突:

EnumMap

如果Map的key是enum类型,推荐使用EnumMap,既保证速度,也不浪费空间。

它在内部以一个非常紧凑的数组存储value(一个枚举元素对应一个数组下标),并且根据enum类型的key直接定位到内部数组的索引,并不需要计算hashCode(),不但效率最高,而且没有额外的空间浪费。

使用EnumMap的时候,根据面向抽象编程的原则,应持有Map接口:Map<DayOfWeek, String> map = new EnumMap<>(DayOfWeek.class);

TreeMap

有序Map,按照比较器来进行排序,不是按照保存顺序排序。

Map -> SortedMap -> TreeMap

在内部会对Key进行排序,使用TreeMap时,放入的Key必须实现Comparable接口,或者自定义一个比较器。如果a==b,则返回0,如果a>b,则返回1,如果a<b,则返回-1TreeMap内部根据比较结果对Key进行排序,从小打到进行存储。

 

Person类并未覆写equals()hashCode(),因为TreeMap不使用equals()hashCode()。直接按照比较器对key进行排序,按顺序存储,比较器返回负数表示当前值小于正在比较的值,目标位置在比较值之前;比较器返回0表示当前值等于正在比较的值,如果是保存:就直接覆盖正在比较的值,如果是读取,则返回正在比较的值;比较器返回正数表示当前值大于正在比较的值,目标位置在比较值之后。所以不用比较哈希值和Key值。

TreeMap在按照Key取值时是:依赖Key的compareTo()方法或者Comparator.compare()方法返回0来判定Key值是否相等。

Properties

本质为Map<String, String>:

  1. 创建Properties实例;Properties props = new Properties();

  2. 调用load()读取文件;

    props.load(new java.io.FileInputStream(filePath));//默认ASCII编码,中文乱码

    props.load(new FileReader(filePath, StandardCharsets.UTF_8));// 使用utf-8编码,防止乱码

  3. 调用getProperty()获取配置:

    String value = props.getProperty("name");

    String value = props.getProperty("name","ass"); // name不存在时,返回默认值ass

  4. 写入数据:props.setProperty("name", "value");

  5. 保存文件:props.store(new FileOutputStream("filePath"), "properties注释");如果有多个.properties文件,可以反复调用load()读取,后读取的key-value会覆盖已读取的key-value:可以把默认配置文件放到classpath中,然后,根据机器的环境编写另一个配置文件,覆盖某些默认的配置。

set

创建Set:Set.of()

只需要存储不重复的key,并不需要存储映射的value,Set实际上相当于只存储key、不存储value的Map,底层实现也是直接用的HashMap。我们经常用Set用于去除重复元素。放入Set的元素和Map的key类似,都要正确实现equals()hashCode()方法:以便对key进行比较,去重。

Set接口并不保证有序:HashSet不保证有序,而SortedSet接口则保证元素是有序的:TreeSet保证有序。添加到TreeSet的元素必须正确实现Comparable接口,如果没有实现Comparable接口(照元素的排序顺序),那么创建TreeSet时必须传入一个Comparator对象(自定义排序算法),以便对Key进行排序,不必覆写`equals()hashCode(),因为直接按照比较器对key进行排序,按顺序存储,比较器返回负数表示当前值小于正在比较的值,目标位置在比较值之前;比较器返回0表示当前值等于正在比较的值,如果是保存:就直接覆盖正在比较的值,如果是读取,则返回正在比较的值;比较器返回正数表示当前值大于正在比较的值,目标位置在比较值之后。所以不用比较哈希值和Key值。

Queue

Queue实际上是实现了一个先进先出(FIFO:First In First Out)的有序表。它和List的区别在于,List可以在任意位置添加和删除元素,而Queue只有两个操作:

 throw Exception返回false或null
添加元素到队尾add(E e)boolean offer(E e)
取队首元素并删除E remove()E poll()
取队首元素但不删除E element()E peek()

LinkedList即实现了List接口,又实现了Queue接口,但是,在使用的时候,如果我们把它当作List,就获取List的引用,如果我们把它当作Queue,就获取Queue的引用:

要避免把null添加到队列,否则poll()方法返回null时,很难确定是取到了null元素还是队列为空。

PriorityQueue

它的出队顺序与元素的优先级有关,对PriorityQueue调用remove()poll()方法,返回的总是优先级最高的元素。

放入PriorityQueue的元素,必须实现Comparable接口或者传入比较器,PriorityQueue会根据元素的排序顺序从小到大排列。相同优先级的按先后顺序排列。

Deque

实现自Quene,允许两头都进,两头都出

Deque是一个接口,它的实现类有ArrayDequeLinkedList:我们在使用的时候,总是用特定的接口来引用它,这是因为持有接口说明代码的抽象层次更高,而且接口本身定义的方法代表了特定的用途。Deque<String> d = new LinkedList<>();可见面向抽象编程的一个原则就是:尽量持有接口,而不是具体的实现类

由于他实现自Quene,所以存在add(),remove(),element(),poll,offer(),peak()方法,但是为了显示出实际的操作方向,推荐使用firstXXX(),lastXXX()方法。

Stack只有入栈和出栈的操作:

Deque可以实现Stack的功能:

Deque作为Stack使用时,注意只调用push()/pop()/peek()方法,不要调用addFirst()/removeFirst()/peekFirst()方法,这样代码更加清晰。

不要使用遗留类Stack

迭代器

调用方总是以统一的方式遍历各种集合类型,而不必关系它们内部的存储结构,Iterator对象是集合对象自己在内部创建的,它自己知道如何高效遍历内部的数据集合,调用方则获得了统一的代码,编译器才能把标准的for each循环自动转换为Iterator遍历:

想要使用for each循环,只需满足以下条件:

在编写Iterator的时候,我们通常可以用一个内部类来实现Iterator接口,这个内部类可以通过通过这个this引用直接访问对应的外部类的所有字段和方法。

Collections工具类

提供了一系列静态方法,能更方便地操作各种集合。

IO

1:IO流是一种流式的数据输入/输出模型:

File
InputStream

Read方法

操作系统往往会一次性读取若干字节到缓冲区,并维护一个指针指向未读的缓冲区。然后,每次我们调用int read()读取下一个字节时,可以直接返回缓冲区的下一个字节,避免每次读一个字节都导致IO操作。当缓冲区全部读完后继续调用read(),则会触发操作系统的下一次读取并再次填满缓冲区。

ByteArrayInputStream在内存中模拟一个字节流输入,用于测试:

 

OutputStream

ByteArrayOutputStream可以在内存中模拟一个OutputStream

FilterInputStream

FilterInputStream本身不实现输入流的功能,而是通过构造函数传入另一个InputStream的子类,把输入流的功能交给它做。通过继承FilterInputStream可以对输入输出流的行为进行扩展,这是装饰模式的典型用法。通过多个装饰类实现责任链模式,通过装饰模式和责任链模式,在不改变接口的语义的情况下做到了对行为的扩展,它将对一个输入流的不同处理分散到不同的FilterInputStream中去,类似流水线,每个一节点完成一定的任务:InputStream in = new GZIPInputStream(new BufferedInputStream(new FileInputStream("1.txt")));最里面的FileInputStream是真正的数据来源,而BufferedInputStream和GZIPInputStream都继承了FilterInputStream对I/O的行为进行了改变。

无论我们包装多少次,得到的对象始终是InputStream,我们直接用InputStream来引用它,就可以正常读取:

最外层的InputStream关闭时(在try(resource)块的结束处自动关闭),内层的InputStreamclose()方法也会被自动调用,并最终调用到最核心的“基础”InputStream

img

zip操作

要创建一个ZipInputStream,通常是传入一个FileInputStream作为数据源,然后,循环调用getNextEntry(),直到返回null,表示zip流结束。

一个ZipEntry表示一个压缩文件或目录,如果是压缩文件,我们就用read()方法不断读取,直到返回-1

classpath读取文件

从classpath读取文件就可以避免不同环境下文件路径不一致的问题:如果我们把default.properties文件放到classpath中,就不用关心它的实际存放路径。在classpath中的资源文件,路径总是以开头,先获取当前的Class对象,然后调用getResourceAsStream()就可以直接从classpath读取任意的资源文件:

把默认的配置放到jar包中,再从外部文件系统读取一个可选的配置文件,就可以做到既有默认的配置文件,又可以让用户自己修改配置:

序列化

序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程,反序列化,即把一个二进制内容(也就是byte[]数组)变回Java对象。

象要能序列化,必须实现一个特殊的java.io.Serializable接口,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。

Reader

FileReaderReader的一个子类,它可以打开文件并获取Reader,实现了文件字符流输入

普通的Reader实际上是基于InputStream构造的,它在内部实际上持有一个FileInputStream,因为Reader需要从InputStream中读入字节流(byte),然后,根据编码设置,再转换为char就可以实现字符流。Reader本质上是一个基于InputStreambytechar的转换器,

把任何InputStream转换为Reader

writer

Writer就是带编码转换器的OutputStream,它把char转换为byte并输出。FileWriter就是向文件中写入字符流的Writer

普通的Writer实际上是基于OutputStream构造的,它接收char,然后在内部自动转换成一个或多个byte,并写入OutputStream

OutputStreamWriter就是一个将任意的OutputStream转换为Writer的转换器:

PrintStream

PrintStream是一种FilterOutputStream,它在OutputStream的接口上,额外提供了一些写入各种数据类型的方法,最终输出的总是byte数据:

相较于FilterOutputStream,PrintStream除了添加了一组print()/println()方法,可以打印各种数据类型,不会抛出IOException,就不必捕获IOException

PrintWriter

PrintStream最终输出的总是byte数据,而PrintWriter则是扩展了Writer接口,它的print()/println()方法最终输出的是char数据。使用与PrintStream相同。

Files工具类

读取

写入

Files提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,读写大型文件仍然要使用文件流,每次只读写一部分文件内容。

时间

概念

1:计算机内部时间存储的是Epoch Time,是计算从1970年1月1日零点(格林威治时区/GMT+00:00)到现在所经历的秒数,当需要显示为某一地区的当地时间时,我们就把它格式化为一个字符串,通过System.currentTimeMillis()获取当前时间戳。

本地时间

LocalDateTime表示本地时间没有时区,无法确定某一时刻

3:自定义格式

4:加减

对日期的运算会自动调整闰月:10月3日-1(月)=9月30日

LocalDateTime提供了对日期和时间进行加减的非常简单的链式调用:

对日期和时间进行调整则使用withXxx(n)方法:

要判断两个LocalDateTimeLocalDateLocalTime的先后,可以使用isBefore()isAfter()方法:LocalDateTime.now().isAfter(LocalDateTime.of(2019, 11, 19, 8, 15, 0));

5:时间差

Duration表示两个时刻之间的时间间隔。`Period表示两个日期之间的天数:

ZonedDateTime

ZonedDateTime理解成LocalDateTimeZoneId

7: 时区转换

格式化日期

DateTimeFormatter不但是不变对象,它还是线程安全的,只创建一个实例,到处引用。

时间戳

数据库中存储时间戳时,尽量使用long型时间戳,它具有省空间,效率高,不依赖数据库的优点。

LocalDateTimeZoneIdInstantZonedDateTimelong都可以互相转换:

10 :旧API转新API

通过toInstant()方法转换为Instant对象,再继续转换为ZonedDateTime