集合框架
集合框架是Java用来存放数据的容器,主要分为基于链式存储Collection系列和键值对存储的Map系列,它们的继承树如下。
集合的遍历
使用Lambda表达式遍历Collection集合
Iterable接口新增了一个forEach(Consumer action)方法,又因为Iterable接口是Collection的父接口所以Collection可以直接调用forEach来遍历集合。同时Consumer是一个函数式接口所以我们使用Lambda表达式来遍历集合,代码如下。
package collectionanditerator; import java.util.Collection; import java.util.HashSet; public class CollectionEach { public static void main(String[] args) { Collection books = new HashSet(); books.add("数学之美"); books.add("文明之光"); books.add("大学之路"); books.forEach((obj)->{System.out.println("集合框架元素: "+obj);}); //forEach需要的是一个对象 这个类是一个函数式接口所以可以使用lambda表达式 //forEach里面是一个lambda代表Consuomer ,里面的形参是obj总有人会把实参传进去 这个动作会发生在forEach循环的内部 //事实上forEach会把这个集合中的每一个元素都拿出来送给Customer中的accept方法 这是实参 } }
使用Iterator来遍历集合
Iterator也是集合框架的重要对象但是与Map和Collection不同它不是用来盛装元素的而是用来遍历集合的。同时它有一个重要特征就是它是线程安全的,在Iterator遍历的时候只有Iterator能够操作集合,如果Collection这时也正在操作集合那么将会抛出异常。其次,每迭代一次next的指针会往后移动一位,所以一个Iterator只能够遍历一次集合。最后,与for-each循环一样在Iterator遍历的时候只是拿到了集合中的值而非集合中的元素本身。
package collectionanditerator; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; public class IteratorTest { public static void main(String[] args) { Collection<String> c = new HashSet<String>(); c.add("谷歌方法论"); c.add("二叉树"); c.add("疯狂Java"); Iterator it = c.iterator(); while(it.hasNext()) { String str = (String) it.next();//它只是拿出了和集合中相同的的值而不是集本身 //所以修改值是没有用的 这一点与foreach一样 System.out.println(str); if(str.equals("疯狂Java")) { it.remove();//从集合中删除上一次next返回的元素 // c.remove("疯狂Java");此处有可能会报错 } } c.forEach(obj->System.out.println("迭代 "+obj)); } }
在Java8中新增了一个forEachRemaining(Consumer action)方法来迭代集合,Consumer 又是一个函数式接口,所以我们又可以用Lambda表达式来遍历集合以下代码为Lambda表达式遍历Iterator。
package collectionanditerator; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; public class IteratorTest2 { public static void main(String[] args) { Collection<String> c = new HashSet<String>(); c.add("谷歌方法论"); c.add("二叉树"); c.add("疯狂Java"); c.iterator().forEachRemaining((obj)->{System.out.println("迭代 ------>"+obj);}); } }
集合中的过滤器Predicate
Predicate也是一个函数式接口,它里面的Test方法会对一些事物进行判断,因此可以在它的内部写上一些判断的逻辑然后进行一些过滤的操作。比如说删除集合中长度小于7的字符串。
package predicatetest.test; import java.util.Collection; import java.util.HashSet; public class PredicateTest { public static void main(String[] args) { Collection<String> c = new HashSet<String>(); c.add("谷歌方法论"); c.add("二叉树"); c.add("疯狂Java"); c.add("认知方法论"); c.add("Think In Java"); c.add("古典方法论"); //实参就是迭代的当前对象 c.removeIf(ele->((String)ele).length()<7); System.out.println(c); } }
还可以用这些过滤条件做一些统计,比如统计集合中含有“Java”的字串,也比如说统计集合中长度大于7的字串。
package predicatetest.test; import java.util.Collection; import java.util.HashSet; import java.util.function.Predicate; public class PredicateTest2 { public static void main(String[] args) { Collection<String> c = new HashSet<String>(); c.add("谷歌方法论"); c.add("二叉树"); c.add("疯狂Java"); c.add("认知方法论"); c.add("Think In Java"); c.add("古典方法论"); System.out.println(calAll(c,ele->((String)ele).contains("Java"))); System.out.println(calAll(c,ele->((String)ele).length()>7)); //要使用Lambda表达式前提是要有一个方法需要一个函数式接口的对象,在这里没有所以要先定义一个 } public static int calAll(Collection books,Predicate p) { int total = 0; for(Object o : books) { if(p.test(o)) { total++; } } return total; } }
操作集合Stream
Java8新增了Stream、IntStream、LongStream等流式API。Stream为通用接口流,IntStream、LongStream分别为int、long的流。这些流可以直接拿来做统计功能及其强大!下面介绍IntStream的简单使用。需要注意的就是这个流在一次使用完之后就关闭了。
package stream.test; import java.util.stream.IntStream; public class IntStreamTest { public static void main(String[] args) { IntStream is = IntStream.builder() .add(-2) .add(15) .add(65) .add(18) .build(); //System.out.println(is.max().getAsInt());//得到最大值 //IntStream stream = is.map(ele-> ele *2);//它返回了一个int然后被加到了一个新的集合中去 //stream.forEach(ele->System.out.println(ele)); System.out.println(is.anyMatch(ele-> ele>20)); } }
用Collection直接获取Stream
在Collection集合中可以直接调用.stream()方法就能够得到一个操作流。通过操作流又可以进行统计,判断,过滤等操作。代码如下:
package stream.test; import java.util.Collection; import java.util.HashSet; public class CollectionStreamTest { public static void main(String[] args) { Collection books = new HashSet(); books.add("数学之美"); books.add("文明之光"); books.add("大学之路"); //结合Predicate统计集合中长度>4的字串 System.out.println(books.stream().anyMatch(ele->((String)ele).length()>4)); //统计其中包含 之 的字串数目 System.out.println(books.stream().filter(ele -> ((String)ele).contains("之")).count()); //将books中的字串长度返回形成一个IntStream然后在用forEach遍历 books.stream().mapToInt(ele->((String)ele).length()).forEach(ele -> System.out.println(ele)); } }
Stream大大简化了集合编程。
Set集合
Set集合与Collection基本相同只是Set中不容许有相同的元素。它的实现类有HashSet,TreeSet,EnumSet我们在后面逐一讨论。
HashSet
HashSet的存储使用的的Hash算法,即当有一个对象要插入时首先计算它的Hash码然后把它存放到对应的位置。这也就使得HsahSet集合的查找元素的速度非常快。判断一个元素是否在集合中只要计算这个对象的HashCode然后直接去找这个元素。
由于HashSet不允许重复的元素,这就要对正在插入的元素进行判断。在这里两个元素相等的充要条件就是HashCode相等而且这两个元素equals缺一不可!下面介绍两种特殊的情况。
①HashCode一样但不equals,结果是这个元素将会被存在与它HashCode相同的位置的链表上。这种情况降低了HashSet的查找性能。
②HashCode不同但是equals,结果就是Set集合会认为他们是不相同的元素会被存放在Set集合的不同位置。
③HashCode相同也equals这样的话就会认为他们相同。
④HashCode不同也不equals这样的话会认为他们不同。
以下的代码说明了这个问题:
package set.hashset; import java.util.HashSet; import java.util.Set; class A{//hashCode不同而equals 放在set集合的不同位置 public boolean equals(Object o) { return true; } } class B{//hashCode相同但是 不equals所以放在set集合中的同一个位置 但是由于hashCode同一个上有很多不同的对象所以在此处延伸出一个链表来表示 public int hashCode() { return 1; } } class C{//两个指标都相等所以在set集合上一一对应 public boolean equals(Object o) { return true; } public int hashCode() { return 2; } } public class HashSetTest { public static void main(String[] args) { Set s = new HashSet(); s.add(new A()); s.add(new A()); s.add(new B()); s.add(new B()); s.add(new C()); s.add(new C()); s.forEach(ele -> System.out.println(ele)); } /*输出 set.hashset.A@7852e922 set.hashset.B@1 set.hashset.B@1 set.hashset.C@2 set.hashset.A@4e25154f */ }
这个故事告诉我们在当一个对象要装入Set集合中的时候,如果要重写hashCode与equals方法一定要保证他们的同步。
HashSet中的可变对象
当Set集合中装有可变对象的时候,如果将对象的属性进行改变那将会发生非常麻烦的事情。代码如下:
package set.hashset; import java.util.HashSet; import java.util.Iterator; import java.util.Set; class R{ int count; public R(int count){ this.count = count; } public String toString() { return "R[count:"+count+"]"; } public boolean equals(Object obj) { if(this==obj) { return true; } if(obj!=null&&obj.getClass()==R.class) { R r = (R)obj; return this.count == r.count; } return false; } public int hashCode() { return count; } } public class OverrideHashCode { public static void main(String[] args) { Set set = new HashSet(); set.add(new R(5)); set.add(new R(-2)); set.add(new R(-3)); set.add(new R(9)); Iterator it = set.iterator(); set.forEach(ele -> System.out.print(ele+" ")); //R[count:-2] R[count:-3] R[count:5] R[count:9] System.out.println(); R first = (R) it.next(); first.count = -3;//它还占据着HashCode为-2的位置 set.forEach(ele -> System.out.print(ele+" ")); //R[count:-3] R[count:-3] R[count:5] R[count:9] System.out.println(); set.remove(new R(-3));//只有第二个满足条件所以只删除第二个,因为它占据了hashCode为-3的位置 set.forEach(ele -> System.out.print(ele+" ")); //R[count:-3] R[count:5] R[count:9] System.out.println(); System.out.println(set.contains(new R(-3)));//hashCode不同但是equals System.out.println(set.contains(new R(-2)));//hashCode相同但是不equals } }
LinkedHashSet
LinkedHashSet是HashSet的子类,它的内部是通过链表来维护的,所以它会按照元素插入的顺序保存。也正是由于要维护这些顺序所以它的性能略低于HashSet。当然在迭代访问Set集合中的元素的时候会有较高的性能。
TreeSet
TreeSet是SortedSet接口的实现类。TreeSet可以保证集合处于有序的状态。它的排序方式默认使用自然排序,当然也可以定制排序。除此之外相对于HashSet来说TreeSet还新增了一些方法比如说获取第一个元素,取最后一个元素等等比较简单这里就不在赘述了。下面主要介绍一下TreeSet的自然排序和定制排序。
自然排序机制
一般的TreeSet默认采用自然排序,由于排序所以会存在元素之间的比较这就要求放入TreeSet中的元素都实现Comparable接口。以下是一些常用的比较规则:
①BigDecimal,会找到他们对应的数值包装类型进行比较。
②Character与String都会按照unicode的值来进行比较。
③Date与Time会按照后来时间大于先前时间来比较。
如果没有实现Comparable,同时在比较的时候comparable会将所有要比较的对象都强制转换成同一个对象如果转换失败也会报错。代码如下:
package set.treehset; import java.util.Date; import java.util.TreeSet; public class TreeSetTest1 { public static void main(String[] args) { TreeSet set = new TreeSet();//默认情况下是有序的所以需要调用compareTo方法(而且只有实现了compareable方法后才能往TreeSet里面加) set.add(new String());//java.lang.String cannot be cast to java.util.Date set.add(new Object());//没有实现comparable接口 这里会报出 java.lang.Object cannot be cast to java.lang.Comparable set.add(new Date());//虽然都实现了comparable接口但是类型不一致 } }
当compareTo总是返回非0值
当compareTo总是返回非0值的时候,即使是一个相同的元素也会被加到TreeSet集合中来这个时候集合中的引用指向的就是同一个对象。这就建议我们在编程的时候要保证equals与compareTo的一致性。代码如下:
package set.treehset; import java.util.TreeSet; public class ChaosTreeSet { public static void main(String[] args) { //一种不推荐的编码 TreeSet set = new TreeSet(); Z z1 = new Z(6); set.add(z1); System.out.println(set.add(z1)); System.out.println(set); ((Z)set.first()).age = 9; System.out.println(((Z)set.last()).age);//两个引用指向了同一个实例对象,由于compareTo的返回值总是1 //在两个对象实际的值不同的但compareTo为0时候在插入的时候会失败,这与我们的意图相违背 //实务上在开发过程中我们总是要让equals与compareTo的返回值同步 } } class Z implements Comparable{//为什么没有提示呢? int age; public Z(int age) { this.age = age; } public int compareTo(Object o) { return 1; } public boolean equals() { return true; } }
TreeSet与可变对象
当TreeSet中含有可变对象的时候,如果在将所有可变对象装入之后改变其中对象值的属性这样会使得TreeSet的显示和访问非常混乱。这也就是要提醒我们不要轻易在改变TreeSet集合中可变对象的属性。代码如下:
package set.treehset; import java.util.TreeSet; class R implements Comparable{ int count; public R(int count){ this.count = count; } public String toString() { return "R[count:"+count+"]"; } public boolean equals(Object obj) { if(this==obj) { return true; } if(obj!=null&&obj.getClass()==R.class) { R r = (R)obj; return this.count == r.count; } return false; } public int compareTo(Object obj) { R r = (R)obj; return this.count > r.count? 1 : this.count < r.count? -1:0; } } public class TreeSetTest3 {//这个演示的是在添加了可变对象的时候在中途改变对象会发生什么 public static void main(String[] args) { TreeSet set = new TreeSet(); set.add(new R(5)); set.add(new R(4)); set.add(new R(3)); set.add(new R(2)); System.out.println(set); ((R)set.first()).count = 20;//中途改变了变量而在set中的自然排序不会改变,即一旦将元素装入集合中它的顺序也就定下来了 System.out.println(set); ((R)set.last()).count = 3; System.out.println(set); set.remove(new R(3)); System.out.println(set); //这是一个正常的现象,只是在实际的编程当中我们要避免把改动的对象放在TreeSet集合中 } }
TreeSet集合的自定义排序
自定义排序的TreeSet的定义需要用到带有参数Comparator的构造器。而且添加的类不需要实现Comparable接口。比如现在要实现一个倒序的TreeSet集合,代码如下:
package set.treehset; import java.util.Comparator; import java.util.TreeSet; class M{//这个时候该类没有实现Comaprable接口 应为定制的的比较由我们自己来定 int age; public M(int age) { this.age = age; } public String toString() { return "M [age: "+age+"]"; } } public class CustomizationSortTreeSet {//用定制排序完成降序 public static void main(String[] args) { TreeSet set = new TreeSet((o1,o2)->{ M m1 = (M)o1; M m2 = (M)o2; return m1.age>m2.age?-1:m1.age<m2.age?1:0; }); set.add(new M(5)); set.add(new M(8)); set.add(new M(15)); set.forEach((ele)->{System.out.println(ele);}); } }
EnumSet
EnumSet最特殊的地方在于它的初始化需要指定一个特殊的枚举类,而且在指定好枚举类之后所有添加的对象都必须是该枚举类的枚举值。具体用法如下:
package set.enumset; import java.util.EnumSet; enum Season{ SPRING,SUMMER,FALL,WINTER } public class EnumSetTest { public static void main(String[] args) { EnumSet set1 = EnumSet.allOf(Season.class);//创建一个指定枚举类的set满集合 System.out.println(set1); EnumSet set2 = EnumSet.noneOf(Season.class);//创建一个指定枚举类的空集合 System.out.println(set2); set2.add(Season.SPRING); set2.add(Season.FALL); EnumSet set3 = EnumSet.of(Season.SUMMER,Season.WINTER);//添加两个枚举值 System.out.println(set3); EnumSet set4 = EnumSet.complementOf(set3); System.out.println(set4); //从。。到。。左闭右开 EnumSet set5 = EnumSet.range(Season.SPRING,Season.WINTER); System.out.println(set5); } }
除此之外EnumSet还可以由Collection集合拷贝得来,代码如下:
package set.enumset; import java.util.Collection; import java.util.EnumSet; import java.util.HashSet; enum Season1{//有毛病啊 有点意思 SPRING,SUMMER,FALL,WINTER } public class Collection2EnumSet { public static void main(String[] args) { Collection c = new HashSet(); c.add(Season1.SPRING); c.add(Season1.SUMMER); EnumSet set = EnumSet.copyOf(c); System.out.println(set); } }
各Set类的性能分析
HashSet总是比TreeSet的性能好(尤其是在添加和查询的时候),所以一般推荐使用HashSet集合。如果需要考虑有序排列的话那么就用TreeSet集合比较好。
LinkedHashSet由于底层由链表实现所以会维护插入的顺序,这样它的插入删除操作要略低于HashSet但是在遍历所有元素的时候会更快一些。
EnumSet是所有集合中性能最好的一个集合,但是他只能够保存同一个枚举类中的枚举值。
最后他们都不是线程安全的,所以可以使用Collections类中的synchronizedSortedSet来包装Set集合。
List集合
List集合此处就不在赘述了,只强调List集合中判断两个元素是否相等只需要equals返回true就ok,同时在remove方法中参数是谁就用谁的equals方法。
Java8新增方法
在Java8中List集合增加了sort()排序,和replaceAll()替换所有元素两个常用方法。其中sort()需要指定一个Comparator控制排序的方式。replaceAll()方法则需要一个UnaryOperator来替换集合中所有的元素。代码如下:
package list; import java.util.ArrayList; import java.util.List; public class Java8ArrayList { public static void main(String[] args) { List books = new ArrayList(); books.add(new String("Google方法论")); books.add(new String("数学之美")); books.add(new String("剑指offer")); books.sort((v1,v2)->((String)v1).length()-((String)v2).length()); System.out.println(books); books.replaceAll(ele->((String)ele).length()*2); System.out.println(books); } }
ListIterator迭代器
没什么好说的只是增加了一个向前迭代的功能。
ArrayList和Vector
ArrayList与Vector的用法基本一样,不同的是ArrayList线程不安全,Vector线程安全。所以Vector性能较低。其中Vector有一个子类Stack也就是我们熟知的栈,遵循后进先出的准则。它也是线程安全的所以性能也较低。栈的实现推荐使用ArrayDeque或LinkedList。
固定长度的List
在Java中有Arrays这个类,其中有一个方法Arrays.asList(...)。可以将一些元素或者是数组编程一个List集合。这个集合非util包下的ArrayList集合,而是Arrays的内部类。Arrays.ArrayList是一个固定长度的类,所以不能增加或删除这个集合中的元素。
Queue
Queue为队列接口,基本原则是先进先出。它的实现类有PriorityQueue类。除此之外它还有一个子接口Deque。Deque代表一个双端队列,可以同时在队列的两头来操作集合,因此它也可以当做栈来使用。Deque接口又有两个实现类就是ArrayDeque和LinkedList,下面分别介绍。
PriorityQueue(优先队列)
优先队列是一个不完全标准的Queue的实现类,因为它不满足先进先出的原则。在PriorityQueue中会按照元素的大小来重新排序。当然它的排序也有自然排序或者定制排序两中方式这里就不在赘述了。当使用poll或peek来取出元素时取出的是做小值。示例代码如下:
package Queue; import java.util.PriorityQueue; class Child{ int age; public Child(int age) { this.age = age; } } public class PriorityQueueTest { public static void main(String[] args) { PriorityQueue queue = new PriorityQueue();//这个队列的顺序是自动排好的所以从0开始 //在没有定制排序的时候必须Queue就会用到添加对象的Comparable接口 所以以下的代码会报错 /*queue.offer(new Child(6)); queue.offer(new Child(7)); queue.offer(new Child(8)); queue.offer(new Child(0));*/ System.out.println(queue.poll()); PriorityQueue queue1 = new PriorityQueue((a,b)->{ Child a1 = (Child)a; Child b1= (Child)b; return a1.age>b1.age?-1:a1.age<b1.age?1:0; }); queue1.offer(new Child(6)); queue1.offer(new Child(7)); queue1.offer(new Child(8)); queue1.offer(new Child(0)); queue1.forEach((ele)->{System.out.println(ele);}); PriorityQueue queue2 = new PriorityQueue((a,b)->{ Integer a1 = (Integer)a; Integer b1= (Integer)b; return a1>b1?-1:a1<b1?1:0; }); queue2.offer(6); queue2.offer(7); queue2.offer(8); queue2.offer(9); queue2.forEach((ele)->{System.out.println(ele);}); } }
Deque与ArrayDeque实现类
Deque接口是一个双端的队列,ArrayDeque是它的一个经典实现。从名字就可以看出,ArrayDeque是一个基于数组实现的双端队列。一般当我们用到栈这种数据结构时推荐使用ArrayDeque因为它的效率要比Stack高。下面展示ArrayDeque当做栈来使用:
package Queue; import java.util.ArrayDeque; import java.util.Deque; public class DequeStack {//他是一个双向队列 public static void main(String[] args) { //基本的用法查文档 这里强调它可以当做一个栈来使用 Deque dqueue = new ArrayDeque(); dqueue.push("Linear"); dqueue.push("Logistic"); dqueue.push("CART"); System.out.println(dqueue); System.out.println(dqueue.pop()); System.out.println(dqueue); } }
当然ArrayDeque也可以实现队列的经典功能:
package Queue; import java.util.ArrayDeque; public class DequeQueue { public static void main(String[] args) { ArrayDeque deque = new ArrayDeque(); deque.offer("666"); deque.offer("777"); deque.offer("888"); System.out.println(deque); System.out.println(deque.peek());//队头出队 System.out.println(deque.pollLast());//队尾出队 } }
LinkedList
LinkedList的功能十分强大,它可以作为List集合(可以通过索引来随机访问)、双端队列、栈。同样他是Deque的子类。下面代码展示了它的用法:
package Queue; import java.util.LinkedList; import java.util.List; public class LinkedListTest { public static void main(String[] args) { LinkedList list = new LinkedList(); //既可以是一个List又可以是一个Deque(双向队列),又因为Deque是一个可以是一个栈所以LinkedList也是一个栈 list.offer("111"); list.offer("222"); list.offer("333"); System.out.println(list); list.push("444"); System.out.println(list);//把队头当做是栈顶 list.addFirst("999"); System.out.println(list);//把元素放到队头 System.out.println(list.pop());//出栈 } }
说说效率
LinkedList与ArrayList和ArrayDeque的实现机制完全不同。前者是通过链表来实现的所以访问的效率不如后两者。后两者的底层是数组所以增删的性能较差。底层为数组的一些实现在访问时使用随机访问(get(index))的效率要比Iterator的高。但是LinkedList要使用Iterator的效率会更高。增删评率高的要用LinkedList这样就无需重新分配内存空间了。
Map集合
Map集合大家都很熟悉了他是一个基于k-v映射的集合。下面就直接阐述一下Map的底层。
下图战士的就是Map集合的底层实现。所有的key组成一个Set集合,所有的value组成一个List集合只是索引值不再是正数而是用一个对象来做索引值,一对key-value组成一个Entry。从Java的源码来看Java先实现了一个Map集合然后将所有的value的值赋值为null来实现一个Set集合。
下面是一些Map集合的迭代操作:
package map; import java.util.HashMap; import java.util.Map; public class MapIterator { public static void main(String[] args) { Map map = new HashMap(); map.put("machine","吴恩达"); map.put("learning","林轩田"); map.put("knn", "马士兵"); //两种迭代方式 entrySet与keySet map.entrySet().forEach((ele)->{System.out.println(((Map.Entry)ele).getKey()+" "+((Map.Entry)ele).getValue());}); map.keySet().forEach((ele)->{System.out.println(ele+" "+map.get(ele));}); } }
Java8新增的一些方法
由于我见识短浅所以目前不知道这些新方法有什么太大的用处。以下之挑选了一些作为简单的示例:
package map; import java.util.HashMap; import java.util.Map; public class MapNewTest { public static void main(String[] args) { Map map = new HashMap(); map.put("machine","吴恩达"); map.put("learning","林轩田"); map.put("knn", "马士兵"); // map.put("", value) System.out.println(map); map.merge("knn","老马", (oldVal,param)->((String)oldVal+(String)param)); System.out.println(map); map.computeIfAbsent("Java",(key)->((String)key).length());//原来有值的话不变 System.out.println(map);//如果新计算的value有值的话就put } }
HashMap与Hashtable
Hashtable是一个古老的Map类,它与HashMap的关系完全类似于Vector与ArrayList的关系。由于Hashtable做到了线程同步所以它的效率比较低,除此之外它还包含了一些繁琐的方法所以一般不推荐使用。Hashtable与HashMap还有一个不同的地方就是HashMap允许null为key但是Hashtable不允许。
由于Map集合的key基于Set集合所以作为key的对象一定要实现equals方法和hashCode方法而且要实现它们之间的同步。同样的如果key是可变对象也不要随便改动,改动之后会出现无法准确查询和修改的情景。
同样Hash相关的事物一般都贵在查找速度,因为当检查一个元素是否包含在key中可以直接计算这个元素的Hash值然后在极端的时间内找到对应的值。
IdentityHashMap
IdentityHashMap与HashMap基本相同,不同的只是IdentityHashMap中的key之间是否相等看的是它的IdentityHashMap,也就是只有两个对象绝对相等的时候才会有key的相等。
LinkedHashMap
LinkedHashMap是HashMap的子类,它与HashMap不同的就是它用双向链表链表来维护了key-value的次序。所以它的性能略低于HashMap,但是它在迭代访问时性能较高。以下代码展示了LinkedHashMap的迭代:
package map; import java.util.LinkedHashMap; public class LinkedHashMapTest { public static void main(String[] args) { LinkedHashMap map = new LinkedHashMap(); map.put("machine",80); map.put("learning",100); map.put("sequence",777); map.forEach((key,value)->{System.out.println(key+"---->"+value);}); System.out.println(map);//维护了顺序 } }
使用Properties读写配置文件
Properties的子类是Hashtable的子类,它将.properties配置文件与Map集合关联起来。实务上.properties内部的存储方式就是基于key-value的,更具体的它相当于一个key-value都为String的Map。下面是Properties的简单使用:
package map; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; import java.util.Properties; public class PropertiesTest { public static void main(String[] args) throws FileNotFoundException, IOException { Properties prop = new Properties(); prop.load(PropertiesTest.class.getClassLoader().getResourceAsStream("map/my.properties"));//到达了编译后的根路径也就是bin String age = (String) prop.get("age"); prop.setProperty("dream", "data scientist"); prop.store(new FileWriter("src/map/my.properties"), "hyl` cmd");//默认是当前工程路径 System.out.println(age); } }
这里要强调以下路径的问题。这个.properties文件放在我的src的map包下面。在程序未编译的时候它就会在"src/map/my.properties",当它编译之后它就会在bin下面新生一个副本此时bin的目录也就是我们所说的类路径,当我们拿到类加载器的时候我们所在的目录也就是bin。
SortedMap接口和TreeMap实现类
正如SortedSet接口派生出了TreeSet,SortedMap也派生出了TreeMap。这里的Tree还是值红黑树它根据key值维持了Map集合的有序性,所以它的效率较低。同时TreeMap的排序方式也有两种一种是自然排序,一种是自定义排序。这也要求了在没有自定义排序时参与构建key的对象必须实现Comparable接口。再次强调Java源码就是先实现了HashMap和TreeMap集合然后通过包装所有value为null的Map集合实现了Set。TreeMap的用法比较简单,它只是在HashMap的基础上增加了获取第一个,前一个,最后一个key-value的方法并提供了在其中截取子Map的方法。以下代码是一个TreeMap的用例:
package map; import java.util.TreeMap; class R implements Comparable{ int count; public R(int count) { this.count = count; } public String toString() { return "[count:"+count+"]"; } public boolean equals(Object obj) { if(this==obj) { return true; } if(obj!=null&&obj.getClass()==this.getClass()) { R r = (R)obj; return this.count == r.count; } return false; } @Override public int compareTo(Object o) { R r = (R)o; return count>r.count?1:count<r.count?-1:0; } } public class TreeMapTest { public static void main(String[] args) { TreeMap map = new TreeMap(); map.put(new R(7),8); map.put(new R(-1),10); map.put(new R(9),7); System.out.println(map); map.forEach((key,value)->{System.out.println(((R)key).count+"---->"+value);});//得到了一个有序的结果 System.out.println(map.firstEntry()); System.out.println(map.lastKey()); System.out.println(map.higherKey(new R(2))); System.out.println(map.lowerKey(new R(2))); System.out.println(map.subMap(new R(-2),new R(9))); } }
WeakHashMap
一听到weak这个单词我们不由地想起了Java中4中引用对象的弱引用。它的意义在于能够在内存紧张的时候JVM能够随意的回收一些关于key-value的键值对。但前提是这些能够被释放的Entry没有被施加强引用。以下为实例代码:
package map; import java.util.WeakHashMap; public class WeakHashMapTest { public static void main(String[] args) { WeakHashMap map = new WeakHashMap(); map.put(new String("Machine Learning"), new String("Machine")); map.put("Java", "老马"); System.out.println(map); System.gc(); System.runFinalization(); System.out.println(map);//第一个Entry被回收 } }
EnumMap
没什么好说的,它的初始化需要带有一个Enum类,key只能是该Enum类所对应的枚举值。看用例:
package map; import java.util.EnumMap; enum Season{ SPRING,SUMMER,FULL,WINTER } public class EnumMapTest { public static void main(String[] args) { EnumMap enumMap = new EnumMap(Season.class); enumMap.put(Season.SPRING,"春天"); enumMap.put(Season.SUMMER,"夏天"); System.out.println(enumMap); } }
HashMap和HashSet的性能选项
HashSet和HashMap他们都是采用hash算法来决定集合中的位置。hash算法据定了HashSet中元素的位置,也决定了HashMap或HashTable中key的位置。Hash表中储存元素的表叫做桶一般情况下每一个桶只放一个元素,当然在Hash冲突的时候会放多个元素这样就降低了访问的查找的效率。下图为hash冲突:
HashSet和HashMap的hash表有如下属性:
①容量:hash中桶的数量。
②初始化容量,在创建HashMap(HashSet)时可以在构造器中指定桶的数量。
③尺寸:表示hash中记录的数量。
④负载因子:记录数/容量。
⑤负载极限:当负载因子大于某一个阈值的时候hash表会自动成被的增长空间。既快要装满的时候就会扩容。较高的负载极限会增加空间的利用率省了空间但是费了时间。较低的负载极限费了空间但是省了时间。如果一开始就知道要装多少元素的话可以给hash表分配适当的空间从而避免重新分配。
Collections工具
Collections可以说是一个非常强大的框架,它可以让集合中的元素排序、替换、让集合不可变等操作。以下为用例:
package collections; import java.util.ArrayList; import java.util.Collections; public class CollectionsTest { public static void main(String[] args) { ArrayList list = new ArrayList(); list.add(8); list.add(7); list.add(-7); list.add(-8); System.out.println(list); System.out.println(Collections.max(list)); Collections.replaceAll(list, 8,9); System.out.println(list); Collections.sort(list); System.out.println(list); System.out.println(Collections.binarySearch(list, 7)); } }
创建不可变集合
package collections; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class CollectionsTest2 { public static void main(String[] args) { ArrayList list = new ArrayList(); list.add(8); list.add(7); list.add(-7); list.add(-8); List list1 = Collections.unmodifiableList(list);//不是原来的list成为了不可变的而是新的得到的list成为了不可变的 // list1.add(6); 此处报错 System.out.println(list); List list2 = Collections.singletonList("疯狂Java讲义");//一个不可变得只有一个元素的list } }
除此之外它常常用于打造一个线程安全的集合:
package collections; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class CollectionsTest3 { public static void main(String[] args) { //一个线程安全的list List list = Collections.synchronizedList(new ArrayList()); } }