首页 文章 精选 留言 我的

精选列表

搜索[容器],共10000篇文章
优秀的个人博客,低调大师

Java容器深入浅出之Map、HashMap、Hashtable及其它实现类

在Java中,Set的底层事实上是基于Map实现的,Map内部封装了一个Entry内部接口,由实现类来封装key-value对,当value值均为null时,key的集合就形成了Set。因此,Map集合具有如下的一些特点: 1. Key集因为是Set的实现,因此是无顺序、不可重复的。 2. Value集是List的实现,因此是可以重复的,每个元素根据key来索引。 3. Map内部包含一个Entry内部接口,用于定义key-value对,由实现类来对外提供查找和设置value的方法。 Map的基本功能如下: 1 public class TestMapBasic { 2 3 public static void main(String[] args) { 4 Map<String, Integer> map = new HashMap<>(); 5 //添加键值对,value可重复 6 map.put("AAA", 110); 7 map.put("BBB", 120); 8 map.put("CCC", 20); 9 map.put("DDD", 120); 10 //添加重复key的时候,value会覆盖旧值,方法返回旧值 11 System.out.println(map.put("CCC", 111)); 12 System.out.println(map); 13 //通过key和value查找是否存在对应的键\值 14 System.out.println("map.containsKey(\"BBB\")?: " + map.containsKey("BBB")); 15 System.out.println("map.containsValue(110)?: " + map.containsValue(110)); 16 //遍历Map的增强for循环 17 for(String key:map.keySet()) { 18 System.out.println(key + "-->" + map.get(key)); 19 } 20 //根据key删除value 21 map.remove("AAA"); 22 System.out.println(map); 23 } 24 } Map的Java8增强方法示例如下: 1 public class TestMapAdvance { 2 3 public static void main(String[] args) { 4 Map<String, Integer> map = new HashMap<>(); 5 //添加键值对,value可重复 6 map.put("AAA", 110); 7 map.put("BBB", 120); 8 map.put("CCC", 20); 9 map.put("DDD", 120); 10 //因为map中无ZZZ,因此替换失败 11 map.replace("ZZZ", 122); 12 System.out.println(map); 13 //使用原value与参入参数重新计算value 14 map.merge("CCC", 20, (oldVal, param) -> (oldVal+param)*2); 15 System.out.println(map); 16 //当key为Java,对应的value不存在或null, 计算结果为新value 17 map.computeIfAbsent("Java", (key) -> key.length()); 18 System.out.println(map); 19 //当key为Java存在, 则用新结果替换 20 map.computeIfPresent("Java", (key, value) -> value * 100); 21 System.out.println(map); 22 } 23 } HashMap和Hashtable HashMap和Hashtable都是Map接口的典型实现类,两者的区别在于: 1. HashMap是线程不安全的,性能较高,可以使用null作为key或者value 2. Hashtable是线程安全的,不可以用null做key和value。 与HashSet一样,HashMap和Hashtable不能保证key-value对的顺序,判断两个元素是否相等的标准为: 1. 两个key的equals方法比较返回true 2. 两个key的HashCode方法值相同 HashMap和Hashtable的containsValue()方法,用于判断是否包含指定的value,判断标准为两个value的equals方法相等即可。因此: 1. 使用自定义类作为Key时,如果重写equals和HashCode方法,需确保判断标准一致。 2. 尽量不要使用可变对象作为Key,如果确实需要则不要在程序中修改可变对象。 LinkedHashMap LinkedHashMap在底层使用双链表来维护键值对的顺序,因而性能略低于HashMap,但在迭代集合元素时有较好的性能。 Properties Properties是Hashtable的子类,相当于一个key和value都是String的Map。使用Properties可以方便地把属性文件的“属性名=属性值”转化为Map的键值对,还可以实现Map的键值对与XML文件之间的相互转化,广泛用于JDBC等应用场景中。 1 public class TestProperties { 2 3 public static void main(String[] args) throws FileNotFoundException, IOException { 4 Properties props = new Properties(); 5 //为Properties对象添加元素 6 props.setProperty("username", "admin"); 7 props.setProperty("password", "123456"); 8 //写入文件 9 props.store(new FileOutputStream("test.properties"), "comment line"); 10 Properties props2 = new Properties(); 11 props2.setProperty("gender", "male"); 12 //将文件中的内容追加到Props2的元素中 13 props2.load(new FileInputStream("test.properties")); 14 System.out.println(props2); 15 } 16 17 } TreeMap TreeMap是SortedMap接口的实现类,同时也是TreeSet类的底层实现模型。因此具有与TreeSet相似的性质: 1. TreeMap底层由红黑树的数据结构所实现,每个键值对即为红黑树的一个节点。TreeMap存储键值对时,需要根据Key对节点进行排序。 2. TreeMap的排序方式包括两种: 2.1 自然排序:通过Key所实现的Comparable接口来实现,要求Key都是同一个类的对象 2.2 定制排序:创建TreeMap时,传入一个Comparator对象,负责对Key元素进行排序。 3. 如果使用自定义类作为Key,重写该类的equals方法和compareTo方法应该一致:equals方法返回true时,compareTo方法要返回0。 与HashSet相似,HashMap因为存储的键值对是有序的,因此提供了访问第一个、前一个、第一个、最后一个键值对的方法,以及若干截取子TreeMap的方法。 1 class R implements Comparable<Object>{ 2 3 int count; 4 5 public R(int count) { 6 super(); 7 this.count = count; 8 } 9 10 @Override 11 public String toString() { 12 return "R [count=" + count + "]"; 13 } 14 15 @Override 16 public boolean equals(Object obj) { 17 if (this == obj) 18 return true; 19 if (obj == null) 20 return false; 21 if (getClass() != obj.getClass()) 22 return false; 23 R other = (R) obj; 24 if (count != other.count) 25 return false; 26 return true; 27 } 28 29 @Override 30 public int compareTo(Object o) { 31 R r = (R)o; 32 return count > r.count ? 1: count < r.count ? -1 : 0; 33 } 34 } 35 36 public class TestTreeMap { 37 38 public static void main(String[] args) { 39 TreeMap<R, String> tm = new TreeMap<>(); 40 tm.put(new R(3), "AAAA"); 41 tm.put(new R(-5), "BBB"); 42 tm.put(new R(9), "CCCCCC"); 43 System.out.println(tm); 44 //返回第一个Entry对象 45 System.out.println(tm.firstEntry()); 46 //返回最后一个Key 47 System.out.println(tm.lastKey()); 48 //返回比R(2)大一个的Key 49 System.out.println(tm.higherKey(new R(2))); 50 //返回比R(2)小的一个Key 51 System.out.println(tm.lowerKey(new R(2))); 52 //获取子串 53 System.out.println(tm.subMap(new R(-1), new R(4))); 54 System.out.println(tm); 55 } 56 } View Code WeakHashMap WeakHashMap与HashMap的用法基本相似,区别在于: 1. HashMap的Key保留的是对实际对象的强引用,只要HashMap对象不销毁,所有Key引用对象就不会被垃圾回收;HashMap也不会自动删除这些Key的键值对。 2. WeakHashMap的Key保留的是对实际对象的弱引用,意味着如果这些Key没有被其它强引用对象所引用,Key对象可能会被垃圾回收;WeakHashMap对象也可能会删除这些键值对。 1 public class TestWeakHashMap { 2 3 public static void main(String[] args) { 4 WeakHashMap<String, String> whm = new WeakHashMap<>(); 5 //添加的三个Key对象都是弱引用的匿名类对象 6 whm.put(new String("Python"), new String("pass")); 7 whm.put(new String("R"), new String("good")); 8 whm.put(new String("Scala"), new String("great")); 9 //添加一个系统缓存的字符串直接量, 为强引用对象,作为Key 10 whm.put("Java", new String("Number One")); 11 System.out.println(whm); 12 //通知系统进行垃圾回收 13 System.gc(); 14 System.runFinalization(); 15 //此时whm仅保留一个强引用的Java键值对 16 System.out.println(whm); 17 } 18 } IdentityHashMap IdentityHashMap与HashMap的基本性质和用法大致相同,如可以添加null元素,存储元素的无序性等。核心区别在于判断两个Key对象的标准: IdentityHashMap严格要求两个Key必须是Key1 == Key2的严格相等,即堆内存地址相同。 1 public class TestIdentityHashMap { 2 3 public static void main(String[] args) { 4 IdentityHashMap<String, Integer> ihm = new IdentityHashMap<>(); 5 //两个new String对象,会被认为是不同的Key,一起添加到集合中 6 ihm.put(new String("Python"), 100); 7 ihm.put(new String("Python"), 98); 8 //字符串直接量Java的内存地址相同,会被认为是同一个对象, 因此只会添加一个(覆盖前值) 9 ihm.put("Java", 100); 10 ihm.put("Java", 11); 11 System.out.println(ihm); 12 } 13 } 总结 一般的使用中,为了获取较方便的快速查询,使用HashMap较多。特别地,如果需要一个总是排好序的Key-Value集合,则考虑TreeMap。通过TreeMap的KeySet可以直接获取Key集合,然后通过Arrays的工具类转为数组,就可以通过二分法快速查找已经排序完成的对象。

优秀的个人博客,低调大师

阿里云容器Kubernetes监控(八) - 使用Prometheus实现应用自定义监控

前言 在上一篇文章中为大家讲解了如何在Kubernetes集群中部署Prometheus,已经可以通过Prometheus监控Kubernetes中Pod的状态、核心组件的状态等数据。那么如何将应用自身的数据进行集成呢? Prometheus数据格式解析 Prometheus是通过pull模式进行数据采集的,如果需要接入Prometheus的数据采集,需要符合Prometheus的数据格式,一个标准的Prometheus格式的监控数据格式如下: # TYPE rpc_durations_seconds summary rpc_durations_seconds{service="exponential",quantile="0.5"} 7.55823964126038e-07 rpc_durations_seconds{service="

优秀的个人博客,低调大师

阿里云容器Kubernetes监控(六) - 使用eventer与npd实时告警节点异常

前言 在开始给大家讲解如何通过eventer与npd来实现节点异常告警之前,要稍微给大家解释一下为什么用三篇的篇幅来介绍eventer。在kubernetes中,会将交付场景中的大部分实体都抽象为一个逻辑的概念,例如:接入层抽象为Service,存储层抽象为PV/PVC,不同种类的应用抽象为Deployment、StatefulSet等等。这种抽象的方式不仅仅将交付变成了软件定义式的配置,更多的是规约了一种标准化,这种标准化不仅仅是交付内容的标准化,也包括了交付方式的标准化,甚至交付生命周期的标准化。 交付内容的标准化与交付方式的标准化是非常好理解的,那么交付生命周期的标准化怎么理解呢。我们可以通过kubectl describe deploy [deploy name]的方式查看一个Deployment的状态描述。 在这个例子中,我们

优秀的个人博客,低调大师

在阿里云容器服务上基于Istio实现东西向流量管理

概述 使用 Ingress 将Kubernetes中的应用暴露成对外提供的服务,针对这个对外暴露的服务可以实现灰度发布、流量管理等。我们把这种流量管理称之为南北向流量管理,也就是入口请求到集群服务的流量管理。 而Istio是侧重于集群内服务之间的东西向流量管理、或者称之为服务网格之间的流量管理。 Istio是一个用于连接/管理以及安全化微服务的开放平台,提供了一种简单的方式用于创建微服务网络,并提供负载均衡、服务间认证以及监控等能力,并且关键的一点是并不需要修改服务本身就可以实现上述功能。 该样例应用由四个单独的微服务构成,用来演示多种 Istio 特性。该应用模仿某银行金融产品的一个分类,显示某一金融产品的信息。页面上会显示该产品的描述、明细,以及针对特定用户的增值服务。 四个单独的微服务: • productpage :produc

优秀的个人博客,低调大师

【Java入门提高篇】Day31 Java容器类详解(十三)TreeSet详解

上一篇很水的介绍完了TreeMap,这一篇来看看更水的TreeSet。 本文将从以下几个角度进行展开: 1、TreeSet简介和使用栗子 2、TreeSet源码分析 本篇大约需食用10分钟,各位看官请随意享用。 一、TreeSet简介 TreeSet是Set家族中的又一名懒将,跟其他两位一样,与对应的Map关系密不可分 我们先来回顾一下其他两个Set类,HashSet借助于HashMap拥有快速元素插入和查找的特性,LinkedHashSet借助于LinkedHashMap拥有快速插入查找以及使元素保持插入顺序的特性,TreeSet则是借助于TreeMap拥有使内部元素保持有序的特性,当然,所有的Set集合类都有元素去重的特性。当然,要区别一下的是,TreeSet中的有序是指可以按照内部比较器或者外部比较器的顺序对插入的元素进行排序,也就是每次插入后都会调整顺序以保持内部元素整体有序,而LinkedHashSet只能保持元素的插入顺序。 Talkischeap,showme your code.嗯,还是来看代码吧: public class TreeSetTest { public static void main(String[] args){ TreeSet<String> treeSet = new TreeSet<>(); treeSet.add("Frank"); treeSet.add("Alice"); treeSet.add("Bob"); treeSet.add("Allen"); treeSet.add("Ada"); treeSet.add("Adora"); System.out.println(treeSet); LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>(); linkedHashSet.add("Frank"); linkedHashSet.add("Alice"); linkedHashSet.add("Bob"); linkedHashSet.add("Allen"); linkedHashSet.add("Ada"); linkedHashSet.add("Adora"); System.out.println(linkedHashSet); } } 输出如下: [Ada, Adora, Alice, Allen, Bob, Frank] [Frank, Alice, Bob, Allen, Ada, Adora] 可以看到TreeSet给插入的元素自动排序了。那么可不可以放入我们自定义的类元素呢?当然是可以的,不然要它何用 public class TreeSetTest { public static void main(String[] args){ List<Goods> goods = new ArrayList<>(); goods.add(new Goods("Iphone4S",500.00)); goods.add(new Goods("Iphone5",800.00)); goods.add(new Goods("Iphone6S",2500.00)); goods.add(new Goods("Iphone7S",4500.00)); goods.add(new Goods("Iphone8",6500.00)); goods.add(new Goods("IphoneX",8500.00)); System.out.println(goods); TreeSet<Goods> treeSet = new TreeSet<>(); treeSet.addAll(goods); System.out.println(treeSet); LinkedHashSet<Goods> linkedHashSet = new LinkedHashSet<>(); linkedHashSet.addAll(goods); System.out.println(linkedHashSet); } public static class Goods{ private String name; private Double price; public Goods(String name, Double price) { this.name = name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } @Override public String toString() { return "Goods{" + "name='" + name + '\'' + ", price=" + price + '}'; } } } 输出如下: Exception in thread "main" java.lang.ClassCastException: com.frank.chapter22.TreeSetTest$Goods cannot be cast to java.lang.Comparable [Goods{name='Iphone4S', price=500.0}, Goods{name='Iphone5', price=800.0}, Goods{name='Iphone6S', price=2500.0}, Goods{name='Iphone7S', price=4500.0}, Goods{name='Iphone8', price=6500.0}, Goods{name='IphoneX', price=8500.0}] at java.util.TreeMap.compare(TreeMap.java:1294) at java.util.TreeMap.put(TreeMap.java:538) at java.util.TreeSet.add(TreeSet.java:255) at java.util.AbstractCollection.addAll(AbstractCollection.java:344) at java.util.TreeSet.addAll(TreeSet.java:312) at com.frank.chapter22.TreeSetTest.main(TreeSetTest.java:25) 欢迎来到大型翻车现场。。。 别慌别慌,问题不大。TreeSet与TreeMap一样,是需要元素实现Comparable接口或者传入一个外部比较器的。为什么String可以不用?看看String的实现吧,人家可是实现了Comparable接口的。 所以有两种方式解决,一种是让Goods类实现Comparable接口,另一种是传入一个外部比较器,我们先来看第一种: public class TreeSetTest { public static void main(String[] args){ List<Goods> goods = new ArrayList<>(); goods.add(new Goods("Iphone7S",4500.00)); goods.add(new Goods("Iphone4S",500.00)); goods.add(new Goods("Iphone5",800.00)); goods.add(new Goods("IphoneX",8500.00)); goods.add(new Goods("Iphone6S",2500.00)); goods.add(new Goods("Iphone8",6500.00)); goods.add(new Goods("Iphone8",6500.00)); System.out.println(goods); TreeSet<Goods> treeSet = new TreeSet<>(); treeSet.addAll(goods); System.out.println(treeSet); LinkedHashSet<Goods> linkedHashSet = new LinkedHashSet<>(); linkedHashSet.addAll(goods); System.out.println(linkedHashSet); } public static class Goods implements Comparable{ private String name; private Double price; public Goods(String name, Double price) { this.name = name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } @Override public String toString() { return "Goods{" + "name='" + name + '\'' + ", price=" + price + '}'; } @Override public int compareTo(Object o) { if (!(o instanceof Goods)){ return -1; } Goods goods = (Goods) o; return this.price.compareTo(goods.getPrice()); } } } 为了看出效果,把几个商品的顺序调整了一下,输出如下: [Goods{name='Iphone7S', price=4500.0}, Goods{name='Iphone4S', price=500.0}, Goods{name='Iphone5', price=800.0}, Goods{name='IphoneX', price=8500.0}, Goods{name='Iphone6S', price=2500.0}, Goods{name='Iphone8', price=6500.0}, Goods{name='Iphone8', price=6500.0}] [Goods{name='Iphone4S', price=500.0}, Goods{name='Iphone5', price=800.0}, Goods{name='Iphone6S', price=2500.0}, Goods{name='Iphone7S', price=4500.0}, Goods{name='Iphone8', price=6500.0}, Goods{name='IphoneX', price=8500.0}] [Goods{name='Iphone7S', price=4500.0}, Goods{name='Iphone4S', price=500.0}, Goods{name='Iphone5', price=800.0}, Goods{name='IphoneX', price=8500.0}, Goods{name='Iphone6S', price=2500.0}, Goods{name='Iphone8', price=6500.0}, Goods{name='Iphone8', price=6500.0}] 这里我们按价格进行了升序排列,接下来使用外部比较器的方式进行价格的倒序排列: public class TreeSetTest { public static void main(String[] args){ List<Goods> goods = new ArrayList<>(); goods.add(new Goods("Iphone7S",4500.00)); goods.add(new Goods("Iphone4S",500.00)); goods.add(new Goods("Iphone5",800.00)); goods.add(new Goods("IphoneX",8500.00)); goods.add(new Goods("Iphone6S",2500.00)); goods.add(new Goods("Iphone8",6500.00)); goods.add(new Goods("Iphone8",6500.00)); System.out.println(goods); // 1、使用Lamada表达式 //TreeSet<Goods> treeSet = new TreeSet<>((g1,g2) -> g2.getPrice().compareTo(g1.getPrice())); // 2、使用匿名内部类 TreeSet<Goods> treeSet = new TreeSet<>(new Comparator<Goods>() { @Override public int compare(Goods o1, Goods o2) { return o2.getPrice().compareTo(o1.getPrice()); } }); treeSet.addAll(goods); System.out.println(treeSet); LinkedHashSet<Goods> linkedHashSet = new LinkedHashSet<>(); linkedHashSet.addAll(goods); System.out.println(linkedHashSet); } public static class Goods{ private String name; private Double price; public Goods(String name, Double price) { this.name = name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } @Override public String toString() { return "Goods{" + "name='" + name + '\'' + ", price=" + price + '}'; } } } 在传入外部比较器的时候,也有很多种姿势,这里还是选了匿名内部类的方式进行传入,因为这里只需要使用一次,Lamada表达式还没有做介绍,这里就先不讲了,欣赏一下就好,先领略一下它的强大。 [Goods{name='Iphone7S', price=4500.0}, Goods{name='Iphone4S', price=500.0}, Goods{name='Iphone5', price=800.0}, Goods{name='IphoneX', price=8500.0}, Goods{name='Iphone6S', price=2500.0}, Goods{name='Iphone8', price=6500.0}, Goods{name='Iphone8', price=6500.0}] [Goods{name='IphoneX', price=8500.0}, Goods{name='Iphone8', price=6500.0}, Goods{name='Iphone7S', price=4500.0}, Goods{name='Iphone6S', price=2500.0}, Goods{name='Iphone5', price=800.0}, Goods{name='Iphone4S', price=500.0}] [Goods{name='Iphone7S', price=4500.0}, Goods{name='Iphone4S', price=500.0}, Goods{name='Iphone5', price=800.0}, Goods{name='IphoneX', price=8500.0}, Goods{name='Iphone6S', price=2500.0}, Goods{name='Iphone8', price=6500.0}, Goods{name='Iphone8', price=6500.0}] 这样,就完成了倒序排列了,很简单吧。 二、TreeSet源码分析 先来看看TreeSet的继承关系图: 跟TreeMap的继承机构差不多,NavigableSet接口中存在大量的导航方法,可以帮助更快定位想要查找的元素,AbstractSet提供Set的部分默认实现,这样只需要实现其它方法即可。 可以看到,TreeSet中的方法并不是很多,除了导航方法之外,就是几个最常用的方法了,如add,addAll,remove,contains。接下来让我们一起看看这几个方法是如何实现的: 先来看看内部成员和构造函数: /** * 内部默默无闻工作的Map */ private transient NavigableMap<E,Object> m; // map中共用的一个value private static final Object PRESENT = new Object(); //默认构造方法,根据其元素的自然顺序进行排序 public TreeSet() { this(new TreeMap<E,Object>()); } //构造一个包含指定 collection 元素的新 TreeSet,它按照其元素的自然顺序进行排序。 public TreeSet(Comparator<? super E> comparator) { this(new TreeMap<>(comparator)); } //构造一个新的空 TreeSet,它根据指定比较器进行排序。 public TreeSet(Collection<? extends E> c) { this(); addAll(c); } //构造一个与指定有序 set 具有相同映射关系和相同排序的新 TreeSet。 public TreeSet(SortedSet<E> s) { this(s.comparator()); addAll(s); } TreeSet(NavigableMap<E,Object> m) { this.m = m; } add方法,嗯,够简明扼要。 public boolean add(E e) { return m.put(e, PRESENT)==null; } addAll: public boolean addAll(Collection<? extends E> c) { // 先检测集合是否继承了SortedSet接口,内部map是否为TreeMap // 并且检测使用的比较器是否与内部Map的比较器一致 // 如果一致的话则使用TreeMap的addAllForTreeSet方法进行批量插入 // addAllForTreeSet方法可以在常量时间对有序元素进行插入 if (m.size()==0 && c.size() > 0 && c instanceof SortedSet && m instanceof TreeMap) { SortedSet<? extends E> set = (SortedSet<? extends E>) c; TreeMap<E,Object> map = (TreeMap<E, Object>) m; Comparator<?> cc = set.comparator(); Comparator<? super E> mc = map.comparator(); if (cc==mc || (cc != null && cc.equals(mc))) { map.addAllForTreeSet(set, PRESENT); return true; } } // 如果不满足条件,则使用父类的addAll方法进行添加 return super.addAll(c); } 嗯,这里会先激进优化,不行再用笨办法一个个添加,因为如果是将大量元素插入TreeMap中相对而言还是比较耗时耗力的,每次插入一个元素都可能导致整体结构的调整,而如果插入的元素刚好是有序的,那么就可以对这个过程进行一次很不错的优化。 public boolean remove(Object o) { return m.remove(o)==PRESENT; } public boolean contains(Object o) { return m.containsKey(o); } remove和contains方法也很简单。而且还带一点粗暴 到此,本篇就结束了,其实也没太多内容,因为TreeSet本身也没有太多东西。当然,了解它的内部结构目的是为了更好的使用它。在遇到问题时,每个知识点就是你手中的一张牌,能不能打好这手牌,先要看你这牌好不好,牌不好的话,再聪明也难翻盘。 . 真正重要的东西,用眼睛是看不见的。

优秀的个人博客,低调大师

【Java入门提高篇】Day30 Java容器类详解(十二)TreeMap详解

今天来看看Map家族的另一名大将——TreeMap。前面已经介绍过Map家族的两名大将,分别是HashMap,LinkedHashMap。HashMap可以高效查找和存储元素,LinkedHashMap可以在高效查找的基础上对元素进行有序遍历,那么TreeMap又有什么特点呢?别急别急,看完这篇你就知道了。 本篇主要从以下几个方面对TreeMap进行介绍: 1、TreeMap的特性以及使用栗子 2、TreeMap继承结构简介 3、TreeMap源码分析 本篇预计食用10分钟,请各位食客合理分配时间。 一、TreeMap的特性以及使用栗子 1. 键值不允许重复2. 默认会对键进行排序,所以键必须实现Comparable接口或者使用外部比较器3. 查找、移除、添加操作的时间复杂度为log(n)4.底层使用的数据结构是红黑树 没错,又是让你欲仙欲死的红黑树,不过不要慌,跟之前介绍HashMap时的红黑树是一毛一样的,所以这一篇里,不打算再做介绍啦,如果对红黑树的内容有些遗忘了,可以动动小手,往前面翻一翻。 先来看一个TreeMap的使用小栗子。 public class TreeMapTest { public static void main(String[] args){ TreeMap<String, Integer> grades = new TreeMap<>(); grades.put("Frank", 100); grades.put("Alice", 95); grades.put("Mary", 90); grades.put("Bob", 85); grades.put("Jack", 90); System.out.println(grades); System.out.println(grades.subMap("Bob", "Jack")); System.out.println(grades.subMap("Bob", true, "Jack", true)); System.out.println(grades.ceilingEntry("Bob")); System.out.println(grades.ceilingKey("Bob")); System.out.println(grades.higherEntry("Bob")); System.out.println(grades.higherKey("Bob")); System.out.println(grades.headMap("Bob")); System.out.println(grades.headMap("Bob", true)); System.out.println(grades.tailMap("Bob")); System.out.println(grades.tailMap("Bob", true)); System.out.println(grades.containsKey("Bob")); System.out.println(grades.containsValue(90)); System.out.println(grades.descendingMap()); System.out.println(grades.descendingKeySet()); } } 输出如下: {Alice=95, Bob=85, Frank=100, Jack=90, Mary=90} {Bob=85, Frank=100} {Bob=85, Frank=100, Jack=90} Bob=85 Bob Frank=100 Frank {Alice=95} {Alice=95, Bob=85} {Bob=85, Frank=100, Jack=90, Mary=90} {Bob=85, Frank=100, Jack=90, Mary=90} true true {Mary=90, Jack=90, Frank=100, Bob=85, Alice=95} [Mary, Jack, Frank, Bob, Alice] 可以看到,放入TreeMap中的元素默认按键值升序排列,这里的键值类型为String,使用String的CompareTo方法进行比较和排序。subMap返回当前Map的子Map,headMap和tailMap也是如此, 二、TreeMap继承结构简介 TreeMap继承自AbstractMap,实现了NavigableMap接口,继承关系图如下: 对于AbstractMap相信大家已经不陌生了,HashMap也是继承自AbstractMap,里面有对Map接口的一些默认实现。这里我们可以看到两个新的接口——SortedMap和NavigableMap。SortedMap接口继承自Map接口,从名字就能看出。SortedMap相比Map接口,憎加了排序的功能,内部的方法也不多,简单了解一下就好了 NavigableMap接口继承自SortedMap接口,主要提供一下导航方法: 说了这么多没啥营养的,接下来还是讲讲真正的干货吧。 三、TreeMap源码分析 JDK 1.8中的TreeMap源码有两千多行,还是比较多的。所以本文并不打算逐句分析所有的源码,而是挑选几个常用的内部类和方法进行分析。这些方法实现的功能分别是查找、遍历、插入、删除等,其他的方法小伙伴们有兴趣可以自己分析。TreeMap实现的核心部分是关于红黑树的实现,其绝大部分的方法基本都是对底层红黑树增、删、查操作的一个封装。就像前面所说,只要弄懂了红黑树原理,TreeMap 就没什么秘密了。关于红黑树的原理,可以参考前面关于HashMap红黑树的文章,本篇文章不会对此展开讨论。 TreeMap的主要数据结构是红黑树,而这红黑树结构的承载者便是内部类Entry,先来看看这个Entry类: static final class Entry<K,V> implements Map.Entry<K,V> { K key; V value; Entry<K,V> left; Entry<K,V> right; Entry<K,V> parent; boolean color = BLACK; /** * 构造函数*/ Entry(K key, V value, Entry<K,V> parent) { this.key = key; this.value = value; this.parent = parent; } public K getKey() { return key; } public V getValue() { return value; } public V setValue(V value) { V oldValue = this.value; this.value = value; return oldValue; } public boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<?,?> e = (Map.Entry<?,?>)o; return valEquals(key,e.getKey()) && valEquals(value,e.getValue()); } public int hashCode() { int keyHash = (key==null ? 0 : key.hashCode()); int valueHash = (value==null ? 0 : value.hashCode()); return keyHash ^ valueHash; } public String toString() { return key + "=" + value; } } 其实内部的结构也很简单,主要有key,value和三个分别指向左孩子,右孩子,父节点的引用,以及用来标识颜色的color成员变量。再来看看TreeMap中的几个重要的成员变量: /** * 外部比较器 */ private final Comparator<? super K> comparator; private transient Entry<K,V> root; /** * 键值对数量 */ private transient int size = 0; private transient int modCount = 0; private static final boolean RED = false; private static final boolean BLACK = true; /** * 键值对集合 */ private transient EntrySet entrySet; /** * 键的集合 */ private transient KeySet<K> navigableKeySet; /** * 倒序Map */ private transient NavigableMap<K,V> descendingMap; comparator用于对map中的键进行排序,root指向红黑树的根节点,size表示键值对的数量,modCount相信已经不陌生了,表示内部结构被修改的次数,RED和BLACK是两个内部常量,即红黑两种颜色,false表示红,true表示黑。entrySet是键值对的集合,navigableKeySet是键的集合,最后一个descendingMap是当前map的一个倒序map。 在TreeMap中有很多内部类,可以先看图了解一下: 前前后后一共18个内部类,不过不要慌,其实里面跟迭代器相关的类就占了一半多(10个),跟子Map相关的类占4个,剩下4个就是跟内部集合相关的了。接下来还是一起来看看那些最常用的方法吧: // 插入元素 public V put(K key, V value) { TreeMap.Entry<K,V> t = root; if (t == null) { // 检查类型以及key是否为null // 如果外部比较器为null,且key也为null则会抛出空指针异常 // 如果TreeMap未设置外部比较器,且传入的对象未实现Comparable接口 // 则会抛出ClassCastException异常 compare(key, key); // type (and possibly null) check // 如果根节点不存在,则用传入的键值对信息生成一个根节点 root = new TreeMap.Entry<>(key, value, null); size = 1; modCount++; return null; } int cmp; TreeMap.Entry<K,V> parent; // split comparator and comparable paths Comparator<? super K> cpr = comparator; if (cpr != null) { do { // 如果外部比较器不为空,则依次与各节点进行比较 parent = t; cmp = cpr.compare(key, t.key); if (cmp < 0) // 小于则与左孩子比较 t = t.left; else if (cmp > 0) // 大于则与右孩子比较 t = t.right; else // 找到相等的key则替换其value return t.setValue(value); // 一直循环,直到待比较的节点为null } while (t != null); } else { // 如果外部比较器为null // 如果key为null则抛出空指针 if (key == null) throw new NullPointerException(); // 如果key未实现comparable接口则会抛出异常 @SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key; do { // 跟上面逻辑类似,只是用key的compareTo方法进行比较,而不是用外部比较器的compare方法 parent = t; cmp = k.compareTo(t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } // 生成键值对 TreeMap.Entry<K,V> e = new TreeMap.Entry<>(key, value, parent); // 连接到当前map的左孩子位置或者右孩子位置 if (cmp < 0) parent.left = e; else parent.right = e; // 插入后的调整 fixAfterInsertion(e); size++; modCount++; return null; } 其实这里的逻辑跟HashMap中TreeNode的插入逻辑十分类似,也是先找到要插入的位置,然后再进行结构调整。这里的结构调整即红黑树的结构调整,在前面HashMap中已经详细介绍过了,这里就不重复介绍了,调整过程是完全一样的。 /** * 插入后的调整 */ private void fixAfterInsertion(TreeMap.Entry<K,V> x) { // 将插入的节点初始化为红色节点 x.color = RED; // 如果x不为null且x不是根节点,且x的父节点是红色,此时祖父节点一定为黑色 while (x != null && x != root && x.parent.color == RED) { // 如果x的父节点为祖父节点的左孩子 if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { // y指向x的叔叔节点 TreeMap.Entry<K,V> y = rightOf(parentOf(parentOf(x))); // 如果叔叔节点也是红色,则进行变色处理 if (colorOf(y) == RED) { // 父节点变成黑色 setColor(parentOf(x), BLACK); // 叔叔节点变成黑色 setColor(y, BLACK); // 祖父节点变成黑色 setColor(parentOf(parentOf(x)), RED); // 将x指向祖父节点,继续往上调整 x = parentOf(parentOf(x)); } else { // 如果叔叔节点是黑色节点 // 如果x是父节点的右孩子 if (x == rightOf(parentOf(x))) { // 将x指向其父节点 x = parentOf(x); // 左旋 rotateLeft(x); } // 将x的父节点置为黑色 setColor(parentOf(x), BLACK); // 将x的祖父节点置为红色 setColor(parentOf(parentOf(x)), RED); // 将祖父节点右旋 rotateRight(parentOf(parentOf(x))); } } else { // 这里类似操作 TreeMap.Entry<K,V> y = leftOf(parentOf(parentOf(x))); if (colorOf(y) == RED) { setColor(parentOf(x), BLACK); setColor(y, BLACK); setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } else { if (x == leftOf(parentOf(x))) { x = parentOf(x); rotateRight(x); } setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); rotateLeft(parentOf(parentOf(x))); } } } root.color = BLACK; } 说完了插入,再来看看删除操作。 // 删除节点 public V remove(Object key) { // 先找到该key对应的键值对 TreeMap.Entry<K,V> p = getEntry(key); if (p == null) // 如果未找到则返回null return null; V oldValue = p.value; // 找到后删除该键值对 deleteEntry(p); return oldValue; } final TreeMap.Entry<K,V> getEntry(Object key) { // 为了性能,卸载了比较器的版本 if (comparator != null) return getEntryUsingComparator(key); if (key == null) throw new NullPointerException(); @SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key; TreeMap.Entry<K,V> p = root; // 使用compareTo方法进行查找 while (p != null) { int cmp = k.compareTo(p.key); if (cmp < 0) p = p.left; else if (cmp > 0) p = p.right; else return p; } return null; } // 使用比较器的getEntry版本。 从getEntry分离以获得性能。 // (对于大多数方法而言,这不值得做,这些方法较少依赖于比较器性能,但在这里是值得的。) final TreeMap.Entry<K,V> getEntryUsingComparator(Object key) { @SuppressWarnings("unchecked") K k = (K) key; Comparator<? super K> cpr = comparator; // 使用比较器进行二分查找 if (cpr != null) { TreeMap.Entry<K,V> p = root; while (p != null) { int cmp = cpr.compare(k, p.key); if (cmp < 0) p = p.left; else if (cmp > 0) p = p.right; else return p; } } return null; } /** * 删除节点,并调整红黑树以保持它的平衡 */ private void deleteEntry(TreeMap.Entry<K,V> p) { modCount++; size--; // 如果p的左右孩子均不为空,则找到p的后继节点,并且将p指向该后继节点 if (p.left != null && p.right != null) { TreeMap.Entry<K,V> s = successor(p); p.key = s.key; p.value = s.value; p = s; } // p has 2 children // 修复替补节点 // 用替补节点替换待删除的节点后,需要对其原来所在位置结构进行修复 TreeMap.Entry<K,V> replacement = (p.left != null ? p.left : p.right); if (replacement != null) { replacement.parent = p.parent; if (p.parent == null) root = replacement; else if (p == p.parent.left) p.parent.left = replacement; else p.parent.right = replacement; p.left = p.right = p.parent = null; // 如果p的颜色是黑色,则进行删除后的修复 if (p.color == BLACK) fixAfterDeletion(replacement); } else if (p.parent == null) { root = null; } else { if (p.color == BLACK) fixAfterDeletion(p); if (p.parent != null) { if (p == p.parent.left) p.parent.left = null; else if (p == p.parent.right) p.parent.right = null; p.parent = null; } } } /** * 返回指定节点的后继节点 */ static <K,V> TreeMap.Entry<K,V> successor(TreeMap.Entry<K,V> t) { if (t == null) return null; else if (t.right != null) { TreeMap.Entry<K,V> p = t.right; // 如果右子树不为空,则找到右子树的最左节点作为后继节点 while (p.left != null) p = p.left; return p; } else { TreeMap.Entry<K,V> p = t.parent; TreeMap.Entry<K,V> ch = t; // 如果右子树为空且当前节点为其父节点的左孩子,则直接返回 // 如果为其父节点的右孩子,则一直往上找,直到找到根节点或者当前节点为其父节点的左孩子时,用其做为后继节点 while (p != null && ch == p.right) { ch = p; p = p.parent; } return p; } } /** * 进行删除后的结构修复 * @param x */ private void fixAfterDeletion(TreeMap.Entry<K,V> x) { while (x != root && colorOf(x) == BLACK) { // 如果x是父节点的左孩子 if (x == leftOf(parentOf(x))) { // sib指向x的兄弟节点 TreeMap.Entry<K,V> sib = rightOf(parentOf(x)); // 如果sib是红色,则进行变色处理 if (colorOf(sib) == RED) { // 兄弟节点改为黑色 setColor(sib, BLACK); // 父节点改为红色 setColor(parentOf(x), RED); // 父节点左旋 rotateLeft(parentOf(x)); // sib指向x的父节点的右孩子 sib = rightOf(parentOf(x)); } // 如果sib的左孩子和右孩子都是黑色,则进行变色处理 if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) { // 将sib置为红色 setColor(sib, RED); // x指向其父节点 x = parentOf(x); } else { // 如果sib的右孩子是黑色而左孩子是红色,则变色右旋 if (colorOf(rightOf(sib)) == BLACK) { setColor(leftOf(sib), BLACK); setColor(sib, RED); rotateRight(sib); sib = rightOf(parentOf(x)); } // 变色左旋 setColor(sib, colorOf(parentOf(x))); setColor(parentOf(x), BLACK); setColor(rightOf(sib), BLACK); rotateLeft(parentOf(x)); x = root; } } else { // symmetric // 跟上面操作类似 TreeMap.Entry<K,V> sib = leftOf(parentOf(x)); if (colorOf(sib) == RED) { setColor(sib, BLACK); setColor(parentOf(x), RED); rotateRight(parentOf(x)); sib = leftOf(parentOf(x)); } if (colorOf(rightOf(sib)) == BLACK && colorOf(leftOf(sib)) == BLACK) { setColor(sib, RED); x = parentOf(x); } else { if (colorOf(leftOf(sib)) == BLACK) { setColor(rightOf(sib), BLACK); setColor(sib, RED); rotateLeft(sib); sib = leftOf(parentOf(x)); } setColor(sib, colorOf(parentOf(x))); setColor(parentOf(x), BLACK); setColor(leftOf(sib), BLACK); rotateRight(parentOf(x)); x = root; } } } setColor(x, BLACK); } 嗯,对比一下HashMap的删除操作,核心步骤是完全一样的,所以可以对照前面的HashMap红黑树详解进行食用。 到此,这一篇就很水的讲完啦= = 最近这段时间烦心事比较多,对发展方向也考虑了很多,想做的事情很多,反而让我止步不前,不过很多事情是急不来的,还是好好写写博客,多做总结分享吧。 机会只留给有准备的人。 真正重要的东西,用眼睛是看不见的。

优秀的个人博客,低调大师

【Java入门提高篇】Day27 Java容器类详解(九)LinkedList详解

这次介绍一下List接口的另一个践行者——LinkedList,这是一位集诸多技能于一身的List接口践行者,可谓十八般武艺,样样精通,栈、队列、双端队列、链表、双向链表都可以用它来模拟,话不多说,赶紧一起来看看吧。 本篇将从以下几个方面对LinkedList进行解析: 1.LinkedList整体结构。 2.LinkedList基本操作使用栗子。 3.LinkedList与ArrayList的对比分析。 4.LinkedList整体源码分析。 LinkedList整体结构 先来看看LinkedList中的结构,LinkedList跟ArrayList不一样,ArrayList中是动态维护了一个数组,所有的操作都是 在该数据上进行操作,而LinkedList中其实是一个个的Node节点,每个Node节点首尾相连。如果你还记得前几篇的内容的话,就应该会想起HashMap中其实也是有Node节点的,但两者还是有比较多不一样的地方,先来看看LinkedList中的Node吧。 private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } } 嗯,其实很简单,里面只有三个成员变量,item用来存储具体的元素信息,next指向下一个Node节点,prev指向上一个Node节点,node节点之间通过next和prev相连接,组成了一个双向链表的形式。 嗯,看图说话应该就很好懂了,LinkedList正是由Node这样首尾相连组成了铁索连环的格局。而HashMap中的Node节点其实是一个单链表的节点,只有指向后一个节点的引用(next),并没有指向前一个节点的引用,回顾一下HashMap的Node节点代码便能发现这一点。 static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } ......省略部分代码 } LinkedList基本操作使用栗子 接下来看看LinkedList中的一些基础操作,以下是一个小栗子: ublic class LinkedListTest { public static void main(String[] args){ // 定义要插入集合的字符串对象 String a = "A", b = "B", c = "C", d = "D", e = "E", f = "F", g = "G"; // 创建List集合 LinkedList<String> list = new LinkedList<>(); // add操作 添加元素 list.add(a); list.add(b); list.add(c); list.add(f); // 迭代器遍历 System.out.println("修改前:"); traverseListByIterator(list); // set操作 // 将索引位置为1的对象修改为对象g list.set(1, g); // 将索引位置为2的对象修改为对象d list.set(2, e); // 新建迭代器进行遍历(注意:迭代器是一次性使用的,遍历到列表尾部之后,无法重置,再次遍历时需要新建迭代器) System.out.println(); System.out.println("set修改后的集合中的元素是:"); traverseListByIterator(list); // 创建ArrayList List<String> strings = new ArrayList<>(); strings.add(d); strings.add(a); strings.add(f); // addAll添加所有元素 list.addAll(strings); //foreach方式遍历 System.out.println("addAll修改后的集合中的元素是:"); traverseListByForEach(list); // remove移除元素 String removeElement = list.remove(); System.out.println("移除的元素是:" + removeElement); System.out.println("remove修改后的集合中的元素是:"); traverseListByForEach(list); // add插入元素(在第四个位置插入元素"gg") list.add(3, "gg"); System.out.println("add插入元素后的集合中的元素是:"); traverseListByForEach(list); } /** * foreach方式遍历列表 * @param list 待遍历的列表 */ private static <T> void traverseListByForEach(List<T> list){ for (T t : list){ System.out.print(t + " "); } System.out.println(); } /** * 迭代器方式遍历列表 * @param list 待遍历的列表 */ private static <T> void traverseListByIterator(List<T> list){ Iterator<T> iterator = list.iterator(); // 循环获取列表中的元素 while (iterator.hasNext()) { System.out.print(iterator.next() + " "); } } } 这里仅仅演示了LinkedList的链表操作,主要有add、addAll、remove、set等,几乎每个常用方法都有重载方法,比如只有一个参数的add方法,会直接将该元素插入到列表的尾部,而有两个参数的add方法,会将元素插入指定位置。 LinkedList与ArrayList的对比分析 也许你会问,ArrayList不是也挺好用的吗,那些操作ArrayList也能做啊,为什么还要用LinkedList呢? 这是一个好问题,ArrayList的最大特点就是能随机访问,因为元素在物理上是连续存储的,所以访问的时候,可以通过简单的算法直接定位到指定位置,所以不管队列的元素数量有多少,总能在O(1)的时间里定位到指定位置,但是连续存储也是它的缺点,导致要在中间插入一个元素的时候,所有之后的元素都要往后挪动。而LinkedList的插入只需要调整前后元素的引用即可。 看图说话,ArrayList插入显然需要进行更多的赋值操作,特别是数组元素个数比较多的时候,会更加明显,如果刚好需要扩容的话,那就会更慢了。而LinkedList只需要将插入位置的前后元素的next或prev引用进行调整即可,而且也没有扩容问题,因为它本身就没有容量的概念,理论上可以无限添加元素。 然后来实际对比一下效率差距: public abstract class TimeCounter { private String name; TimeCounter(String name){ this.name = name; } public void count(){long time = System.currentTimeMillis(); doSomething(); System.out.println(name + " 耗时:" + (System.currentTimeMillis() - time)); } protected abstract void doSomething(); } 先将这个计时抽象成一个模板,每个需要统计耗时的操作只需要继承该类,然后重写doSomeThing方法即可。 public class Test { public static void main(String[] args){ TimeCounter arrayListAddCounter = new TimeCounter("ArrayList add插入到末尾:") { private List<Integer> list = new ArrayList<>(); @Override protected void doSomething() { for (int i = 0; i < 100000; i++) { list.add( i); } } }; TimeCounter linkedListAddCounter = new TimeCounter("LinkedList add插入到末尾:") { private List<Integer> list = new LinkedList<>(); @Override protected void doSomething() { for (int i = 0; i < 100000; i++) { list.add( i); } } }; arrayListAddCounter.count(); linkedListAddCounter.count(); } } ArrayList add插入到末尾: 耗时:11 LinkedList add插入到末尾: 耗时:9 好像勉强符合预期,LinkedList比ArrayList略微快一点,其实如果在ArrayList容量足够的情况下,ArrayList的插入元素到末尾操作是要比LinkedList插入要快的,因为它只需要进行一次赋值即可,而LinkedList还需要先new一个新节点然后再接到链表的最后,这个new的过程看起来微不足道,但是一旦循环次数到达一定量级,开销是不可忽略的。例如把上面的循环次数从100000改成1000000,结果就会变成这样: ArrayList add插入到末尾: 耗时:40 LinkedList add插入到末尾: 耗时:768 这时候,创建节点的开销成了主要时间开销,效率甚至不如ArrayList。我们再换一个插入方式,上面是把元素插入到末尾,这次来把元素插入到首端看看: public class Test { public static void main(String[] args){ TimeCounter arrayListAddCounter = new TimeCounter("ArrayList add插入到首端:") { private List<Integer> list = new ArrayList<>(); @Override protected void doSomething() { for (int i = 0; i < 100000; i++) { list.add(0, i); } } }; TimeCounter linkedListAddCounter = new TimeCounter("LinkedList add插入到首端:") { private List<Integer> list = new LinkedList<>(); @Override protected void doSomething() { for (int i = 0; i < 100000; i++) { list.add(0, i); } } }; arrayListAddCounter.count(); linkedListAddCounter.count(); } } ArrayList add插入到首端: 耗时:607 LinkedList add插入到首端: 耗时:11 当每次都插入在首端时,ArrayList每次都需要进行元素移动,而且列表中元素越多,需要进行移动的次数也越多,在这种情况下,LinkedList是明显优于ArrayList的。 所以说,其实这两者是各有所长各有所短的,一般情况下,选ArrayList就好,除非是需要进行循环操作的次数到达了万的量级的时候,才需要对两者进行选择。也许你会说,既然一般情况下,两者的效率差别不大,那直接用ArrayList就好了,说这么多干嘛呢。哈哈,如果是这样想,那就大错特错了。首先我们不仅需要知其然,还需要知其所以然,如果仅仅知道LinkedList比ArrayList插入效率高,但是却不知道为什么高,高多少,是远远不够的。其次,我们不能仅仅考虑正常情况,对于极端情况也需要有预防措施,对极端情况的思考,正是高手与新手的最大区别。 下面再看看查找元素的比较: public class Test { public static void main(String[] args){ TimeCounter arrayListAddCounter = new TimeCounter("ArrayList get遍历元素:") { private List<Integer> list = new ArrayList<>(); { for (int i = 0; i < 100000; i++) { list.add(i); } } @Override protected void doSomething() { for (int i = 0; i < 100000; i++) { list.get(i); } } }; TimeCounter linkedListAddCounter = new TimeCounter("LinkedList get遍历元素:") { private List<Integer> list = new LinkedList<>(); { for (int i = 0; i < 100000; i++) { list.add(i); } } @Override protected void doSomething() { for (int i = 0; i < 100000; i++) { list.get(i); } } }; arrayListAddCounter.count(); linkedListAddCounter.count(); } } ArrayList get遍历元素: 耗时:3 LinkedList get遍历元素: 耗时:4484 ArrayList完胜,可以看出LinkedList中查找元素是一个十分耗时的操作,甚至比插入元素耗时还要长,因为每次get的时候都是从链表两端进行逐个查找,直到找到指定的位置。想要知道具体细节的话,那就耐心的看看下面的源码解析吧。 LinkedList整体源码分析 先来看看LinkedList的整体结构: 可以看到LinkedList有四个内部类,分别是Node、ListItr、DescendingIterator、LLSpliterator。Node类主要用于存放元素,先来看看Node: private static class Node<E> { //存放元素 E item; //指向下一个Node节点 Node<E> next; //指向上一个Node节点 Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } } 这是可以说是最简单的类了,也构成了LinkedList的基础,LinkedList正是由一个个Node节点连接组成。再来看看ListItr类: //列表迭代器类 private class ListItr implements ListIterator<E> { private Node<E> lastReturned; private Node<E> next; private int nextIndex; private int expectedModCount = modCount; ListItr(int index) { // assert isPositionIndex(index); next = (index == size) ? null : node(index); nextIndex = index; } public boolean hasNext() { return nextIndex < size; } public E next() { checkForComodification(); if (!hasNext()) throw new NoSuchElementException(); lastReturned = next; next = next.next; nextIndex++; return lastReturned.item; } public boolean hasPrevious() { return nextIndex > 0; } public E previous() { checkForComodification(); if (!hasPrevious()) throw new NoSuchElementException(); lastReturned = next = (next == null) ? last : next.prev; nextIndex--; return lastReturned.item; } public int nextIndex() { return nextIndex; } public int previousIndex() { return nextIndex - 1; } public void remove() { checkForComodification(); if (lastReturned == null) throw new IllegalStateException(); Node<E> lastNext = lastReturned.next; unlink(lastReturned); if (next == lastReturned) next = lastNext; else nextIndex--; lastReturned = null; expectedModCount++; } public void set(E e) { if (lastReturned == null) throw new IllegalStateException(); checkForComodification(); lastReturned.item = e; } public void add(E e) { checkForComodification(); lastReturned = null; if (next == null) linkLast(e); else linkBefore(e, next); nextIndex++; expectedModCount++; } public void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); while (modCount == expectedModCount && nextIndex < size) { action.accept(next.item); lastReturned = next; next = next.next; nextIndex++; } checkForComodification(); } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } } 这个类是LinkedList的迭代器类,主要用于顺序遍历LinkedList,在前面的栗子中有使用迭代器的hasNext方法和next方法,其实它们的实现都很简单,hasNext只是简单的比较下一个要访问的元素序号是否大于列表中的元素个数,next方法则是将next的引用赋值给lastReturned变量,然后将next指向其下一个节点并且将index加1。但跟普通迭代器不一样的地方在于这个迭代器不仅可以正序遍历,还可以使用previous方法进行倒序遍历,DescendingIterator便是使用了迭代器的previous方法进行便利的。 /** * 降序迭代器 */ private class DescendingIterator implements Iterator<E> { private final ListItr itr = new ListItr(size()); public boolean hasNext() { return itr.hasPrevious(); } public E next() { return itr.previous(); } public void remove() { itr.remove(); } } 这个迭代器就比较简单了,只是包裹了一个ListItr实例,然后重载了Iterator的几个方法。LLSpliterator是用于并行流的可分割式迭代器,这里就不做过多介绍了。 再来看看常用的几个方法的实现: /** * 添加指定元素到列表尾部 */ public boolean add(E e) { linkLast(e); return true; } /** * 把元素e链接成尾节点 */ void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; } 当插入一个元素时,会new一个Node对象,并将该值放入Node节点中,然后挂到链表的尾部。 /** * 在列表指定位置插入指定元素 */ public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } /** * 插入一个元素到指定非空节点之前 */ void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; } add的重载版本,可以指定序号进行插入,将元素插入到链表中间,进行的操作过程可以联系前面的图进行理解。 /** * 返回指定位置的元素 */ public E get(int index) { checkElementIndex(index); return node(index).item; } /** * 返回指定元素序号的非空节点。 */ Node<E> node(int index) { // assert isElementIndex(index); //做了一个小优化,如果取的序号小于元素个数的一半,则从链表首端开始遍历,否则从链表尾部开始遍历 if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } } 可以看到get方法其实是从头部或者尾部进行遍历定位的,每次将x的引用向后/向前移动一位,当链表数据量比较大时,这个过程其实是很耗时间的,前面的对比中应该也能发现这一点。 /** * 取回并删除此列表的头部(第一个元素)。 */ public E remove() { return removeFirst(); } /** * 移除列表指定位置的元素,如果其后子序列存在,则将其元素左移,将它们的序号减一。 * 返回列表中移除的元素。 */ public E remove(int index) { checkElementIndex(index); return unlink(node(index)); } /** * 移除并返回首节点 */ public E removeFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return unlinkFirst(f); } /** * 移除首节点并返回该节点元素值 */ private E unlinkFirst(Node<E> f) { // assert f == first && f != null; final E element = f.item; final Node<E> next = f.next; f.item = null; f.next = null; // help GC first = next; if (next == null) last = null; else next.prev = null; size--; modCount++; return element; } /** * 移除非空节点x */ E unlink(Node<E> x) { // assert x != null; final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; } remove方法也有两个重载版本,不带参数的remove方法仅仅移除并返回最后一个节点,而指定序号参数的remove方法则会移除并返回链表中指定位置的节点。 此外链表还有可以用于队列的诸多方法: 在尾部添加元素:(add, offer),add()会在长度不够时抛出异常:IllegalStateException; offer()则不会,只返回false。 查看头部元素 (element, peek),返回头部元素,但不改变队列。element()会在没元素时抛出异常:NoSuchElementException; peek()返回null; 删除头部元素 (remove,poll),返回头部元素,并且从队列中删除,remove()会在没元素时抛出异常:NoSuchElementException; poll()返回null; 即可以通过以上方法来实现单向队列的操作,也可以使用addFirst,addLast,removeFirst,removeLast方法来实现双向队列的操作。以下是一个简单队列的实现: public class MyQueue<T> { private LinkedList<T> list = new LinkedList<T>(); //清空队列 public void clear() { list.clear(); } //判断队列是否为空 public boolean isEmpty() { return list.isEmpty(); } //进队 public void enqueue(T o) { list.addLast(o); } //出队 public T dequeue(){ if(!list.isEmpty()) { return list.removeFirst(); } return null; } //获取队列长度 public int length() { return list.size(); } //查看队首元素 public T peek() { return list.getFirst(); } //测试队列 public static void main(String[] args) { MyQueue<String> queue = new MyQueue<String>(); System.out.println(queue.isEmpty()); queue.enqueue("a"); queue.enqueue("b"); queue.enqueue("c"); queue.enqueue("d"); queue.enqueue("e"); queue.enqueue("f"); System.out.println(queue.length()); System.out.println(queue.dequeue()); System.out.println(queue.dequeue()); System.out.println(queue.peek()); System.out.println(queue.dequeue()); queue.clear(); queue.enqueue("s"); queue.enqueue("t"); queue.enqueue("r"); System.out.println(queue.dequeue()); System.out.println(queue.length()); System.out.println(queue.peek()); System.out.println(queue.dequeue()); } } 嗯,其实就是借鸡生蛋的事情嘛,哈哈。 再来看看用LinkedList的简单栈实现: public class MyStack<T> { private LinkedList<T> stack = new LinkedList<T>(); // 入栈 public void push(T v) { stack.addFirst(v); } // 出栈,但不删除 public T peek() { return stack.getFirst(); } // 出栈 public T pop() { return stack.removeFirst(); } // 栈是否为空 public boolean empty() { return stack.isEmpty(); } // 打印栈元素 public String toString() { return stack.toString(); } } 你看,其实很简单吧,LinkedList提供的大量的方法,可以很方便的进行链表、双向链表、队列、双端队列、栈等数据结构的实现,可以说非常好用了。 下面是LinkedList的全部源码,行有余力的话可以选择你想要了解的部分进行阅读,如果有不懂的地方可以在本文后面留言,当然,也可以直接跳过,以后想要深入了解的时候再进行阅读也不迟。 /** * 双向链表实现了List接口和Deque接口,实现了多有可选List操作,并且允许放入所有的元素,包括null。 * * 所有的操作都表现得像双向链表,索引到列表中的操作将从开头或者结尾遍历列表,以较接近指定索引为准。 * * 注意,这个实现类不是线程安全的,如果多个线程同时访问一个链表,并且至少一个线程修改了链表的结构, * 则必须在外部实现同步。结构性修改指的是那些增加删除一个或多个元素的操作,仅设置元素的值不是结构修改。 * 这通常通过在封装列表的某个对象上进行同步来实现。 * * 如果没有这样的对象存在,则列表应该使用Collections#synchronizedList方法包装,最好在创建列表的时候进行, * 以防止意外的非同步引用对列表进行了修改。 * List list = Collections.synchronizedList(new LinkedList(...)); * * 该类的iterator方法和listIterator方法返回的迭代器是“fail-fast”的,如果列表在迭代器创建之后的任何时刻被进行 * 结构性的修改了,则调用迭代器自身的remove或者add方法时将会抛出ConcurrentModificationException异常,因此 * 当遇到并发修改时,迭代器会快速的失败,而不是在未来某个不确定的时刻进行武断冒险或不确定性的行为 * * 注意,通常来说,不能保证迭代器的fail-fast机制,在遇到非同步的并发修改时,不可能做出任何严格的保证。 * fail-fast 迭代器只能尽最大努力抛出ConcurrentModificationException异常,因此,如果程序依赖这个异常来 * 进行正确性判断是错误的,fail-fast机制仅应该用于检测异常。 * */ public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { transient int size = 0; /** * 指向第一个节点 * 恒等式: (first == null && last == null) || * (first.prev == null && first.item != null) */ transient Node<E> first; /** * 指向最后一个节点 * 恒等式: (first == null && last == null) || * (last.next == null && last.item != null) */ transient Node<E> last; /** * 构造一个空的列表 */ public LinkedList() { } /** * 构造一个包含指定集合内所有元素的列表,存储的顺序为集合的迭代器访问顺序。 * @throws NullPointerException 空指针异常 */ public LinkedList(Collection<? extends E> c) { this(); addAll(c); } /** * 把元素e链接成首节点 */ private void linkFirst(E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; } /** * 把元素e链接成尾节点 */ void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; } /** * 插入一个元素到指定非空节点之前 */ void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; } /** * 移除首节点并返回该节点元素值 */ private E unlinkFirst(Node<E> f) { // assert f == first && f != null; final E element = f.item; final Node<E> next = f.next; f.item = null; f.next = null; // help GC first = next; if (next == null) last = null; else next.prev = null; size--; modCount++; return element; } /** * 移除尾节点并返回该节点元素值 */ private E unlinkLast(Node<E> l) { // assert l == last && l != null; final E element = l.item; final Node<E> prev = l.prev; l.item = null; l.prev = null; // help GC last = prev; if (prev == null) first = null; else prev.next = null; size--; modCount++; return element; } /** * 移除非空节点x */ E unlink(Node<E> x) { // assert x != null; final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; } /** * 返回列表的首节点 */ public E getFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return f.item; } /** * 返回列表的最后一个节点 */ public E getLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return l.item; } /** * 移除并返回首节点 */ public E removeFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return unlinkFirst(f); } /** * 移除并返回尾节点 */ public E removeLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return unlinkLast(l); } /** * 插入指定元素到列表首端 */ public void addFirst(E e) { linkFirst(e); } /** * 扩展指定元素到列表尾端 */ public void addLast(E e) { linkLast(e); } /** * 返回列表中是否包含指定元素 */ public boolean contains(Object o) { return indexOf(o) != -1; } /** * 返回列表中元素个数 */ public int size() { return size; } /** * 添加指定元素到列表尾部 */ public boolean add(E e) { linkLast(e); return true; } /** * 如果该元素存在,则移除列表中首次出现的该指定元素,如果不存在,则原链表不会改变。 * 如果该列表中包含该指定元素则返回true,否则返回false */ public boolean remove(Object o) { if (o == null) { for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; } /** * 添加指定集合中的所有元素到列表的尾部,顺序为指定集合的迭代器遍历顺序。如果该操作正在进行时,指定集合 * 被修改了,那么该操作的行为是不可预测的。 */ public boolean addAll(Collection<? extends E> c) { return addAll(size, c); } /** * 将指定集合中的所有元素插入到该列表的指定位置之后。将当前在该位置的元素及其之后的元素右移。新元素在列表 * 中的顺序为其原集合迭代器遍历顺序。 */ public boolean addAll(int index, Collection<? extends E> c) { checkPositionIndex(index); Object[] a = c.toArray(); int numNew = a.length; if (numNew == 0) return false; Node<E> pred, succ; if (index == size) { succ = null; pred = last; } else { succ = node(index); pred = succ.prev; } for (Object o : a) { @SuppressWarnings("unchecked") E e = (E) o; Node<E> newNode = new Node<>(pred, e, null); if (pred == null) first = newNode; else pred.next = newNode; pred = newNode; } if (succ == null) { last = pred; } else { pred.next = succ; succ.prev = pred; } size += numNew; modCount++; return true; } /** * 移除列表中所有元素 */ public void clear() { // 清除节点之间所有元素的链接不是必要的,但是: // - 如果被清除的节点处于不同代之间,可以帮助分代GC。 // - 一定要释放内存,即便有一个迭代器引用 for (Node<E> x = first; x != null; ) { Node<E> next = x.next; x.item = null; x.next = null; x.prev = null; x = next; } first = last = null; size = 0; modCount++; } // 位置访问操作 /** * 返回指定位置的元素 */ public E get(int index) { checkElementIndex(index); return node(index).item; } /** * 使用指定元素替换指定位置的元素 */ public E set(int index, E element) { checkElementIndex(index); Node<E> x = node(index); E oldVal = x.item; x.item = element; return oldVal; } /** * 在列表指定位置插入指定元素,如果该位置有任何元素,则将其后的子序列元素都往右移(将它们的序号都增加1) */ public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } /** * 移除列表指定位置的元素,如果其后子序列存在,则将其元素左移,将它们的序号减一。 * 返回列表中移除的元素。 */ public E remove(int index) { checkElementIndex(index); return unlink(node(index)); } /** * 判断该序号是否在列表中对应位置存在元素 */ private boolean isElementIndex(int index) { return index >= 0 && index < size; } /** * 判断给参数是否是迭代器或者add操作的合法位置 */ private boolean isPositionIndex(int index) { return index >= 0 && index <= size; } /** * 构造一个IndexOutOfBoundsException 异常的详情消息,在错误处理代码的许多可能的重构中, * 这个大纲在服务端和客户端虚拟机中都表现的最好。 */ private String outOfBoundsMsg(int index) { return "Index: "+index+", Size: "+size; } private void checkElementIndex(int index) { if (!isElementIndex(index)) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } private void checkPositionIndex(int index) { if (!isPositionIndex(index)) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } /** * 返回指定元素序号的非空节点。 */ Node<E> node(int index) { // assert isElementIndex(index); //做了一个小优化,如果取的序号小于元素个数的一半,则从链表首端开始遍历,否则从链表尾部开始遍历 if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } } // 搜索操作 /** * 返回指定元素在列表中第一次出现的位置,否则返回-1 */ public int indexOf(Object o) { int index = 0; if (o == null) { for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) return index; index++; } } else { for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) return index; index++; } } return -1; } /** * 返回指定元素在链表中最后一次出现的位置,如果链表中不包含该元素,则返回-1 */ public int lastIndexOf(Object o) { int index = size; if (o == null) { for (Node<E> x = last; x != null; x = x.prev) { index--; if (x.item == null) return index; } } else { for (Node<E> x = last; x != null; x = x.prev) { index--; if (o.equals(x.item)) return index; } } return -1; } // 队列操作 /** * 取回但不删除此列表的头部(第一个元素)。 */ public E peek() { final Node<E> f = first; return (f == null) ? null : f.item; } /** * 取回但不删除此列表的头部(第一个元素)。 * 如果不存在,则会抛出NoSuchElementException异常 */ public E element() { return getFirst(); } /** * 取回并删除此列表的头部(第一个元素)。 * 如果该链表为空,则返回null */ public E poll() { final Node<E> f = first; return (f == null) ? null : unlinkFirst(f); } /** * 取回并删除此列表的头部(第一个元素)。 */ public E remove() { return removeFirst(); } /** * 添加指定元素到链表尾部 */ public boolean offer(E e) { return add(e); } // 双向队列操作 /** * 插入指定元素到队列首部 */ public boolean offerFirst(E e) { addFirst(e); return true; } /** * 插入指定元素到队列尾部 */ public boolean offerLast(E e) { addLast(e); return true; } /** * * 取回但是不删除队列的首元素。如果队列为空则返回null。 */ public E peekFirst() { final Node<E> f = first; return (f == null) ? null : f.item; } /** * 取回但是不删除链表的最后一个元素,如果队列为空,则返回null */ public E peekLast() { final Node<E> l = last; return (l == null) ? null : l.item; } /** * 取回并删除队列的首元素,如果队列为空,则返回null */ public E pollFirst() { final Node<E> f = first; return (f == null) ? null : unlinkFirst(f); } /** * 取回并删除队列的尾元素,如果队列为空,则返回null */ public E pollLast() { final Node<E> l = last; return (l == null) ? null : unlinkLast(l); } /** * 在队列前端插入元素 */ public void push(E e) { addFirst(e); } /** * 删除并返回队列的第一个元素,如果列表为空则抛出NoSuchElementException 异常 */ public E pop() { return removeFirst(); } /** * 删除此队列中第一个出现的指定元素(从头到尾的方式遍历时),如果队列中不包含该元素,则队列不会改变。 */ public boolean removeFirstOccurrence(Object o) { return remove(o); } /** * 删除此队列中最后一次出现的指定元素(从头到尾的方式遍历时),如果队列中不包含该元素,则队列不会改变。 */ public boolean removeLastOccurrence(Object o) { if (o == null) { for (Node<E> x = last; x != null; x = x.prev) { if (x.item == null) { unlink(x); return true; } } } else { for (Node<E> x = last; x != null; x = x.prev) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; } /** * 返回从指定位置开始,以正确的序列迭代该列表的迭代器. * 列表迭代器是“fail-fast”的,如果列表在迭代器创建之后的任何时刻被进行 * 结构性的修改了,则调用迭代器自身的remove或者add方法时将会抛出ConcurrentModificationException异常,因此 * 当遇到并发修改时,迭代器会快速的失败,而不是在未来某个不确定的时刻进行武断冒险或不确定性的行为 * * 注意,通常来说,不能保证迭代器的fail-fast机制,在遇到非同步的并发修改时,不可能做出任何严格的保证。 * fail-fast 迭代器只能尽最大努力抛出ConcurrentModificationException异常,因此,如果程序依赖这个异常来 * 进行正确性判断是错误的,fail-fast机制仅应该用于检测异常。 */ public ListIterator<E> listIterator(int index) { checkPositionIndex(index); return new ListItr(index); } //列表迭代器类 private class ListItr implements ListIterator<E> { //记录上一个返回的节点 private Node<E> lastReturned; //指向下一个节点 private Node<E> next; //下一个节点的序号 private int nextIndex; //用于检测遍历过程中List是否被修改 private int expectedModCount = modCount; ListItr(int index) { // assert isPositionIndex(index); next = (index == size) ? null : node(index); nextIndex = index; } public boolean hasNext() { return nextIndex < size; } public E next() { //检测是否修改 checkForComodification(); if (!hasNext()) throw new NoSuchElementException(); lastReturned = next; next = next.next; nextIndex++; return lastReturned.item; } public boolean hasPrevious() { return nextIndex > 0; } public E previous() { checkForComodification(); if (!hasPrevious()) throw new NoSuchElementException(); lastReturned = next = (next == null) ? last : next.prev; nextIndex--; return lastReturned.item; } public int nextIndex() { return nextIndex; } public int previousIndex() { return nextIndex - 1; } public void remove() { checkForComodification(); if (lastReturned == null) throw new IllegalStateException(); Node<E> lastNext = lastReturned.next; unlink(lastReturned); if (next == lastReturned) next = lastNext; else nextIndex--; lastReturned = null; expectedModCount++; } public void set(E e) { if (lastReturned == null) throw new IllegalStateException(); checkForComodification(); lastReturned.item = e; } public void add(E e) { checkForComodification(); lastReturned = null; if (next == null) linkLast(e); else linkBefore(e, next); nextIndex++; expectedModCount++; } public void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); while (modCount == expectedModCount && nextIndex < size) { action.accept(next.item); lastReturned = next; next = next.next; nextIndex++; } checkForComodification(); } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } } private static class Node<E> { //存放元素 E item; //指向下一个Node节点 Node<E> next; //指向上一个Node节点 Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } } /** * 返回一个倒序遍历的迭代器 */ public Iterator<E> descendingIterator() { return new DescendingIterator(); } /** * 降序迭代器 */ private class DescendingIterator implements Iterator<E> { private final ListItr itr = new ListItr(size()); public boolean hasNext() { return itr.hasPrevious(); } public E next() { return itr.previous(); } public void remove() { itr.remove(); } } private LinkedList<E> superClone() { try { return (LinkedList<E>) super.clone(); } catch (CloneNotSupportedException e) { throw new InternalError(e); } } /** * 浅克隆 */ public Object clone() { LinkedList<E> clone = superClone(); // Put clone into "virgin" state clone.first = clone.last = null; clone.size = 0; clone.modCount = 0; // Initialize clone with our elements for (Node<E> x = first; x != null; x = x.next) clone.add(x.item); return clone; } /** * 返回一个包含列表所有元素的数组,元素的顺序为从第一个到最后一个。 */ public Object[] toArray() { Object[] result = new Object[size]; int i = 0; for (Node<E> x = first; x != null; x = x.next) result[i++] = x.item; return result; } /** * 返回一个包含列表所有元素的数组,元素的顺序为从第一个到最后一个。返回元素数组的类型 * 与指定数组的类型一致。如果列表大小适合指定的数组,则返回该数组。 否则,将为新数组分配指定 * 数组的运行时类型和此列表的大小。 * * 如果列表的空间适合指定的数组(数组比列表有更多的元素),紧跟在列表末尾之后的数组中的元素设置 * 为null(仅当调用者知道列表不包含任何null元素时,这在确定列表长度时很有用。) */ @SuppressWarnings("unchecked") public <T> T[] toArray(T[] a) { if (a.length < size) a = (T[])java.lang.reflect.Array.newInstance( a.getClass().getComponentType(), size); int i = 0; Object[] result = a; for (Node<E> x = first; x != null; x = x.next) result[i++] = x.item; if (a.length > size) a[size] = null; return a; } private static final long serialVersionUID = 876323262645176354L; /** * 序列化 */ private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { // Write out any hidden serialization magic s.defaultWriteObject(); // Write out size s.writeInt(size); // Write out all elements in the proper order. for (Node<E> x = first; x != null; x = x.next) s.writeObject(x.item); } /** * 反序列化 */ @SuppressWarnings("unchecked") private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { // Read in any hidden serialization magic s.defaultReadObject(); // Read in size int size = s.readInt(); // Read in all elements in the proper order. for (int i = 0; i < size; i++) linkLast((E)s.readObject()); } /** * 创建一个可分割的迭代器 */ @Override public Spliterator<E> spliterator() { return new LLSpliterator<E>(this, -1, 0); } /** Spliterators.IteratorSpliterator 的定制版本 */ static final class LLSpliterator<E> implements Spliterator<E> { static final int BATCH_UNIT = 1 << 10; // batch array size increment static final int MAX_BATCH = 1 << 25; // max batch array size; final LinkedList<E> list; // null OK unless traversed Node<E> current; // current node; null until initialized int est; // size estimate; -1 until first needed int expectedModCount; // initialized when est set int batch; // batch size for splits LLSpliterator(LinkedList<E> list, int est, int expectedModCount) { this.list = list; this.est = est; this.expectedModCount = expectedModCount; } final int getEst() { int s; // force initialization final LinkedList<E> lst; if ((s = est) < 0) { if ((lst = list) == null) s = est = 0; else { expectedModCount = lst.modCount; current = lst.first; s = est = lst.size; } } return s; } public long estimateSize() { return (long) getEst(); } public Spliterator<E> trySplit() { Node<E> p; int s = getEst(); if (s > 1 && (p = current) != null) { int n = batch + BATCH_UNIT; if (n > s) n = s; if (n > MAX_BATCH) n = MAX_BATCH; Object[] a = new Object[n]; int j = 0; do { a[j++] = p.item; } while ((p = p.next) != null && j < n); current = p; batch = j; est = s - j; return Spliterators.spliterator(a, 0, j, Spliterator.ORDERED); } return null; } public void forEachRemaining(Consumer<? super E> action) { Node<E> p; int n; if (action == null) throw new NullPointerException(); if ((n = getEst()) > 0 && (p = current) != null) { current = null; est = 0; do { E e = p.item; p = p.next; action.accept(e); } while (p != null && --n > 0); } if (list.modCount != expectedModCount) throw new ConcurrentModificationException(); } public boolean tryAdvance(Consumer<? super E> action) { Node<E> p; if (action == null) throw new NullPointerException(); if (getEst() > 0 && (p = current) != null) { --est; E e = p.item; current = p.next; action.accept(e); if (list.modCount != expectedModCount) throw new ConcurrentModificationException(); return true; } return false; } public int characteristics() { return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED; } } } 最后,再对LinkedList做一个简单的小结: LinkedList是由Node节点首尾相连而成的结构,相比ArrayList而言,在进行插入和删除时不需要进行大量的元素移动,省去了元素复制的开销,也不存在扩容开销,但是每次添加节点都需要创建一个新的Node对象,所以当节点数量很多时,这部分对象将占用很大开销,包括时间成本和空间成本,因此需要根据实际情况进行合理选择。LinkedList因为提供了大量方便的获取元素、插入元素和移除元素的方法,所以可以很方便的进行队列、栈等数据结构的实现。 到此,LinkedList就算讲解完毕了,一不小心又写了这么长,罪过罪过。。。下次还是多分几篇写吧,这么长的文章,确实不便于阅读,面壁中。。。 真正重要的东西,用眼睛是看不见的。

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

用户登录
用户注册