List接口提供了subList方法,其做用是返回一个列表的子列表,这与String类subSting有点相似,但它们的功能是否相同呢?咱们来看以下代码:web
1 public class Client70 { 2 public static void main(String[] args) { 3 // 定义一个包含两个字符串的列表 4 List<String> c = new ArrayList<String>(); 5 c.add("A"); 6 c.add("B"); 7 // 构造一个包含c列表的字符串列表 8 List<String> c1 = new ArrayList<String>(c); 9 // subList生成与c相同的列表 10 List<String> c2 = c.subList(0, c.size()); 11 // c2增长一个元素 12 c2.add("C"); 13 System.out.println("c==c1? " + c.equals(c1)); 14 System.out.println("c==c2? " + c.equals(c2)); 15 } 16 }
c1是经过ArrayList的构造函数建立的,c2是经过列表的subList方法建立的,而后c2又增长了一个元素"C",如今的问题是输出的结果是什么呢?列表c与c一、c2之间是什么关系呢?先不回答这个问题,咱们先来回想一下String类的subString方法,看看它是如何工做的,代码以下: 算法
1 public static void testStr() { 2 String str = "AB"; 3 String str1 = new String(str); 4 String str2 = str.substring(0) + "C"; 5 System.out.println("str==str1? " + str.equals(str1)); 6 System.out.println("str==str2? " + str.equals(str2)); 7 }
很明显,str和str1是相等的(虽然不是同一个对象,但用equals方法判断是相等的),但它们与str2不相等,这毋庸置疑,由于str2在对象池中从新生成了一个新的对象,其表面值是ABC,那固然与str和str1不相等了。数据库
说完了subString的小插曲,如今回到List是否相等的判断上来。subList与subString的输出结果是同样的吗?让事实说话,运行结果以下:c==c1? false c==c2? trueapache
很遗憾,与String类正好相反,一样是一个sub类型的操做,为何会相反呢?c2是经过subList方法从c列表中生成的一个子列表,而后c2又增长了一个元素,可为何增长了一个元素还会相等呢?咱们从subList的源码来分析一下: 编程
1 public List<E> subList(int fromIndex, int toIndex) { 2 return (this instanceof RandomAccess ? 3 new RandomAccessSubList<>(this, fromIndex, toIndex) : 4 new SubList<>(this, fromIndex, toIndex)); 5 }
subList的方法是由AbstractList实现的,它会根据是否是能够随机存取来提供不一样的SubList实现方式,不过,随机存取的使用频率比较高,并且RandomAccessSubList也是subList的子类,因此全部的操做都是由Sublist类实现的(除了自身的SubList方法外),那么,咱们就直接看看SubList类的代码:数组
1 class SubList<E> extends AbstractList<E> { 2 //原始列表 3 private final AbstractList<E> l; 4 //偏移量 5 private final int offset; 6 private int size; 7 //构造函数,注意list参数就是咱们的原始列表 8 SubList(AbstractList<E> list, int fromIndex, int toIndex) { 9 /*下标校验代码 略*/ 10 //传递原始列表 11 l = list; 12 offset = fromIndex; 13 //子列表的长度 14 size = toIndex - fromIndex; 15 this.modCount = l.modCount; 16 } 17 //得到制定位置的元素 18 public E get(int index) { 19 /*下标校验 略*/ 20 //从原始字符串中得到制定位置的元素 21 return l.get(index+offset); 22 } 23 //增长或插入 24 public void add(int index, E element) { 25 /*下标校验 略*/ 26 //直接增长到原始字符串上 27 l.add(index+offset, element); 28 /*处理长度和修改计数器*/ 29 } 30 /*其它方法 略*/ 31 }
经过阅读这段代码,咱们就很是清楚subList方法的实现原理了:它返回的SubList类也是AbstractList的子类,其全部的get、set、add、remove等都是在原始列表上的操做,它自身并无生成一个新的数组或是链表,也就是子列表只是原列表的一个视图(View)而已。全部的修改动做都映射到了原列表上。多线程
咱们例子中的c2增长了一个元素C,不过增长的元素C到了c列表上,两个变量的元素仍然保持一致,相等也就是天然的了。并发
解释完相等的问题,再回过头来看看变量c与c1不行等的缘由,很简单,由于经过ArrayList构造函数建立的List对象其实是新列表,它是经过数组的copyOf动做生成的,所生成的列表c1与原列表c之间没有任何关系(虽然是浅拷贝,但元素类型是String,也就是说元素是深拷贝的),而后c又增长了元素,由于c1与c之间已经没有一毛线关系了。app
注意:subList产生的列表只是一个视图,全部的修改动做直接做用于原列表。 dom
咱们来看这样一个简单的需求:一个列表有100个元素,如今要删除索引位置为20~30的元素。这很简单,一个遍历很快就能够完成,代码以下:
1 public class Client71 { 2 public static void main(String[] args) { 3 // 初始化一个固定长度,不可变列表 4 List<Integer> initData = Collections.nCopies(100, 0); 5 // 转换为可变列表 6 List<Integer> list = new ArrayList<Integer>(initData); 7 // 遍历,删除符合条件的元素 8 for (int i = 0; i < list.size(); i++) { 9 if (i >= 20 && i < 30) { 10 list.remove(i); 11 } 12 } 13 } 14 }
或者将for循环改成:
1 for(int i=20;i<30;i++){ 2 if(i<list.size()){ 3 list.remove(i); 4 } 5 }
相信首先出如今你们脑海中的实现算法就是此算法了,遍历一遍,符合条件的删除,简单而使用,不过,有没有其它方式呢?有没有“one-lining”一行代码就解决问题的方式呢?
有,直接使用ArrayList的removeRange方法不就能够了吗?不过好像不可能呀,虽然JDK上由此方法,可是它有protected关键字修饰着,不能直接使用,那怎么办?看看以下代码:
1 public static void main(String[] args) { 2 // 初始化一个固定长度,不可变列表 3 List<Integer> initData = Collections.nCopies(100, 0); 4 // 转换为可变列表 5 List<Integer> list = new ArrayList<Integer>(initData); 6 //删除指定范围内的元素 7 list.subList(20, 30).clear(); 8 }
上一个建议讲了subList方法的具体实现方式,全部的操做都是在原始列表上进行的,那咱们就用subList先取出一个子列表,而后清空。由于subList返回的list是原始列表的一个视图,删除这个视图中 的全部元素,最终都会反映到原始字符串上,那么一行代码解决问题了。
顺便贴一下上面方法调用的源码:
public void clear() { removeRange(0, size()); }
1 protected void removeRange(int fromIndex, int toIndex) { 2 ListIterator<E> it = listIterator(fromIndex); 3 for (int i=0, n=toIndex-fromIndex; i<n; i++) { 4 it.next(); 5 it.remove(); 6 } 7 }
前面说了,subList生成的子列表是原列表的一个视图,那在subList执行完后,若是修改了原列表的内容会怎样呢?视图是否会改变呢?若是是数据库视图,表数据变动了,视图固然会变了,至于subList生成的视图是否会改变,仍是从源码上来看吧,代码以下:
1 public class Client72 { 2 public static void main(String[] args) { 3 List<String> list = new ArrayList<String>(); 4 list.add("A"); 5 list.add("B"); 6 list.add("C"); 7 List<String> subList = list.subList(0, 2); 8 //原字符串增长一个元素 9 list.add("D"); 10 System.out.println("原列表长度:"+list.size()); 11 System.out.println("子列表长度:"+subList.size()); 12 } 13 }
程序中有一个原始列表,生成了一个子列表,而后在原始列表中增长一个元素,最后打印出原始列表和子列表的长度,你们想一下,这段程序什么地方会出现错误呢?list.add("D")会报错吗?不会,subList并无锁定原列表,原列表固然能够继续修改。难道有size方法?正确,确实是size方法出错了,输出结果以下:
什么,竟然是subList的size方法出现了异常,并且仍是并发修改异常?这没道理呀,这里根本就没有多线程操做,何来并发修改呢?这个问题很容易回答,那是由于subList取出的列表是原列表的一个视图,原数据集(代码中的lsit变量)修改了,可是subList取出的子列表不会从新生成一个新列表(这点与数据库视图是不相同的),后面在对子列表继续操做时,就会检测到修改计数器与预期的不相同,因而就抛出了并发修改异常。出现这个问题的最终缘由仍是在子列表提供的size方法的检查上,还记得上面几个例子中常常提到的修改计数器?缘由就在这里,咱们来看看size的源代码:
1 public int size() { 2 checkForComodification(); 3 return size; 4 }
其中的checkForComodification()方法就是用于检测是否并发修改的,代码以下:
1 private void checkForComodification() 2 { 3 //判断当前修改计数器是否与子列表生成时一致 4 if(modCount != l.modCount) 5 throw new ConcurrentModificationException(); 6 else 7 return; 8 }
modCount 是从什么地方来的呢?它是在subList子列表的构造函数中赋值的,其值等于生成子列表时的修改次数吗。所以在生成子列表后再修改原始列表,l.modCount的值就必然比modeCount大1,再也不保持相等了,因而就抛出了ConcurrentModificationException异常。
subList的其它方法也会检测修改计数器,例如set、get、add等方法,若生成子列表后,再修改原列表,这些方法也会抛出ConcurrentModificationException异常。
对于子列表的操做,由于视图是动态生成的,生成子列表后再操做原列表,必然会致使"视图 "的不稳定,最有效的方法就是经过Collections.unmodifiableList方法设置列表为只读状态,代码以下:
1 public static void main(String[] args) { 2 List<String> list = new ArrayList<String>(); 3 List<String> subList = list.subList(0, 2); 4 //设置列表为只读状态 5 list=Collections.unmodifiableList(list); 6 //对list进行只读操做 7 //...... 8 //对subList进行读写操做 9 //...... 10 }
这在团队编码中特别有用,好比我生成了一个list,须要调用其余同事写的共享方法,可是一些元素是不能修改的,想一想看,此时subList方法和unmodifiableList方法配合使用是否是就能够解决咱们的问题了呢?防护式编程就是教咱们如此作的。
这里还有一个问题,数据库的一张表能够有多个视图,咱们的List也能够有多张视图,也就是能够有多个子列表,但问题是只要生成的子列表多于一个,任何一个子列表都不能修改了,不然就会抛出ConcurrentModificationException异常。
注意:subList生成子列表后,保持原列表的只读状态。
在项目开发中,咱们常常要对一组数据进行排序,或者升序或者降序,在Java中排序有多种方式,最土的方式就是本身写排序算法,好比冒泡排序、快速排序、二叉树排序等,但通常不须要本身写,JDK已经为咱们提供了不少的排序算法,咱们采用"拿来主义" 就成了。在Java中,要想给数据排序,有两种实现方式,一种是实现Comparable接口,一种是实现Comparator接口,这二者有什么区别呢?咱们来看一个例子,就好比给公司职员按照工号排序吧,先定义一个职员类代码,以下所示:
1 import org.apache.commons.lang.builder.CompareToBuilder; 2 import org.apache.commons.lang.builder.ToStringBuilder; 3 public class Employee implements Comparable<Employee> { 4 // 工号--按照进入公司的前后顺序编码的 5 private int id; 6 // 姓名 7 private String name; 8 // 职位 9 private Position position; 10 11 public Employee(int _id, String _name, Position _position) { 12 id = _id; 13 name = _name; 14 position = _position; 15 } 16 //getter和setter方法略 17 // 按照Id排序,也就是按照资历的深浅排序 18 @Override 19 public int compareTo(Employee o) { 20 return new CompareToBuilder().append(id, o.id).toComparison(); 21 } 22 23 @Override 24 public String toString() { 25 return ToStringBuilder.reflectionToString(this); 26 } 27 28 } 29 //枚举类型(三个级别Boss(老板)、经理(Manager)、普通员工(Staff)) 30 enum Position { 31 Boss, Manager, Staff 32 }
这是一个简单的JavaBean,描述的是一个员工的基本信息,其中id是员工编号,按照进入公司的前后顺序编码,position是岗位描述,表示是经理仍是普通职员,这是一个枚举类型。
注意Employee类中的compareTo方法,它是Comparable接口要求必须实现的方法,这里使用apache的工具类来实现,代表是按照Id的天然序列排序的(也就是升序),如今咱们看看如何排序:
1 public static void main(String[] args) { 2 List<Employee> list = new ArrayList<Employee>(5); 3 // 两个职员 4 list.add(new Employee(1004, "马六", Position.Staff)); 5 list.add(new Employee(1005, "赵七", Position.Staff)); 6 // 两个经理 7 list.add(new Employee(1002, "李四", Position.Manager)); 8 list.add(new Employee(1003, "王五", Position.Manager)); 9 // 一个老板 10 list.add(new Employee(1001, "张三", Position.Boss)); 11 // 按照Id排序,也就是按照资历排序 12 Collections.sort(list); 13 for (Employee e : list) { 14 System.out.println(e); 15 } 16 }
在收集数据的时候原本应该从老板到员工,为告终果更清晰,故将其打乱,从员工到老板,排序结果以下:
是按照ID升序排列的,结果正确,可是,有时候咱们但愿按照职位来排序,那怎么作呢?此时,重构Employee类已经不合适了,Employee已是一个稳定类,为了排序功能修改它不是一个好办法,哪有什么好的解决办法吗?
有办法,看Collections.sort方法,它有一个重载方法Collections.sort(List<T> list, Comparator<? super T> c),能够接收一个Comparator实现类,这下就好办了,代码以下:
1 class PositionComparator implements Comparator<Employee> { 2 @Override 3 public int compare(Employee o1, Employee o2) { 4 // 按照职位降序排列 5 return o1.getPosition().compareTo(o2.getPosition()); 6 } 7 }
建立了一个职位排序法,依据职位的高低进行降序排列,而后只要Collections.sort(list)修改成Collections.sort(list,new PositionComparator() )便可实现按照职位排序的要求。
如今问题又来了:按职位临时倒叙排列呢?注意只是临时的,是否须要重写一个排序器呢?彻底不用,有两个解决办法:
第二个问题:先按照职位排序,职位相同再按照工号排序,这如何处理呢?这但是咱们常常遇到的实际问题。很好处理,在compareTo或者compare方法中判断职位是否相等,相等的话再根据工号排序,使用apache工具类来简化处理,代码以下:
@Override public int compareTo(Employee o) { return new CompareToBuilder().append(position, o.position) .append(id, o.id).toComparison(); }
在JDK中,对Collections.sort方法的解释是按照天然顺序进行升序排列,这种说法其实不太准确的,sort方法的排序方式并非一成不变的升序,也多是倒序,这依赖于compareTo的返回值,咱们知道若是compareTo返回负数,代表当前值比对比值小,零表示相等,正数代表当前值比对比值大,好比咱们修改一下Employee的compareTo方法,以下所示:
@Override public int compareTo(Employee o) { return new CompareToBuilder().append(o.id, id).toComparison(); }
两个参数调换了一下位置,也就是compareTo的返回值与以前正好相反,再使用Collections.sort进行排序,顺序也就相反了,这样也实现了倒序。
第三个问题:在Java中,为何要有两个排序接口呢?
其实也很好回答,实现了Comparable接口的类代表自身是能够比较的,有了比较才能进行排序,而Comparator接口是一个工具类接口,它的名字(比较器)也已经代表了它的做用:用做比较,它与原有类的逻辑没有关系,只是实现两个类的比较逻辑,从这方面来讲,一个类能够有不少的比较器,只要有业务需求就能够产生比较器,有比较器就能够产生N多种排序,而Comparable接口的排序只能说是实现类的默认排序算法,一个类稳定、成熟后其compareTo方法基本不会变,也就是说一个类只能有一个固定的、由compareTo方法提供的默认排序算法。
注意:Comparable接口能够做为实现类的默认排序算法,Comparator接口则是一个类的扩展排序工具。
对一个列表进行检索时,咱们使用最多的是indexOf方法,它简单、好用,并且也不会出错,虽然它只能检索到第一个符合条件的值,可是咱们能够生成子列表后再检索,这样也便可以查找出全部符合条件的值了。
Collections工具类也提供了一个检索方法,binarySearch,这个是干什么的?该方法也是对一个列表进行检索的,可查找出指定值的索引,可是在使用这个方法时就有一些注意事项,咱们看以下代码:
1 public class Client74 { 2 public static void main(String[] args) { 3 List<String> cities = new ArrayList<String> (); 4 cities.add("上海"); 5 cities.add("广州"); 6 cities.add("广州"); 7 cities.add("北京"); 8 cities.add("天津"); 9 //indexOf取得索引值 10 int index1= cities.indexOf("广州"); 11 //binarySearch找到索引值 12 int index2= Collections.binarySearch(cities, "广州"); 13 System.out.println("索引值(indexOf):"+index1); 14 System.out.println("索引值(binarySearch):"+index2); 15 } 16 }
先不考虑运行结果,直接看JDK上对binarySearch的描述:使用二分搜索法搜索指定列表,以得到指定对象。其实现的功能与indexOf是相同的,只是使用的是二分法搜索列表,因此估计两种方法返回的结果是同样的,看结果:
索引值(indexOf):1
索引值(binarySearch):2
结果不同,虽然咱们说有两个"广州" 这样的元素,可是返回的结果都应该是1才对呀,为什么binarySearch返回的结果是2呢?问题就出在二分法搜索上,二分法搜索就是“折半折半再折半” 的搜索方法,简单,并且效率高。看看JDK是如何实现的。
1 private static final int BINARYSEARCH_THRESHOLD = 5000; 2 public static <T> 3 int binarySearch(List<? extends Comparable<? super T>> list, T key) { 4 if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD) 5 //随机存取列表或者元素数量少于5000的顺序存取列表 6 return Collections.indexedBinarySearch(list, key); 7 else 8 //元素数量大于5000的顺序存取列表 9 return Collections.iteratorBinarySearch(list, key); 10 }
ArrayList实现了RandomAccess接口,是一个顺序存取列表,使用了indexedBinarySearch方法,代码以下:
1 private static <T> int indexedBinarySearch( 2 List<? extends Comparable<? super T>> list, T key) { 3 // 默认商界 4 int low = 0; 5 // 默认下界 6 int high = list.size() - 1; 7 8 while (low <= high) { 9 //中间索引,无符号右移一位 10 int mid = (low + high) >>> 1; 11 //中间值 12 Comparable<? super T> midVal = list.get(mid); 13 //比较中间值 14 int cmp = midVal.compareTo(key); 15 //重置上界和下界 16 if (cmp < 0) 17 low = mid + 1; 18 else if (cmp > 0) 19 high = mid - 1; 20 else 21 //找到元素 22 return mid; // key found 23 } 24 //没有找到,返回负值 25 return -(low + 1); // key not found 26 }
这也没啥说的,就是二分法搜索的Java版实现。注意看第10和14行代码,首先是得到中间的索引值,咱们的例子中也就是2,那索引值是2的元素值是多少呢?正好是“广州” ,因而就返回索引值2,正确,没问题,咱们再看看indexOf的实现,代码以下:
1 public int indexOf(Object o) { 2 //null元素查找 3 if (o == null) { 4 for (int i = 0; i < size; i++) 5 if (elementData[i]==null) 6 return i; 7 } else { 8 //非null元素查找 9 for (int i = 0; i < size; i++) 10 //两个元素是否相等,注意这里是equals方法 11 if (o.equals(elementData[i])) 12 return i; 13 } 14 return -1; 15 }
indexOf方法就是一个遍历,找到第一个元素值相等则返回,没什么玄机,回到咱们的程序来看,for循环的第二遍便是咱们要查找的 " 广州 " ,因而就返回索引值1了,也正确,没有任何问题。
二者的算法都没有问题,难道是咱们用错了。这的确是咱们使用的错误,由于二分法查询的一个首要前提是:数据集以实现升序排列,不然二分法查找的值是不许确的。不排序怎么肯定是在小区(比中间值小的区域) 中查找仍是在大区(比中间值大的区域)中查找呢?二分法查找必需要先排序,这是二分法查找的首要条件。
问题清楚了,解决办法很easy,使用Collections.sort排下序便可解决。但这样真的能够解决吗?想一想看,元素数据是从web或数据库中传递进来的,本来是一个有规则的业务数据,咱们为了查找一个元素对它进行了排序,也就是改变了元素在列表中的位置,那谁来保证业务规则的准确性呢?因此说,binarySearch方法在此处受限了,固然,拷贝一个数组,而后再排序,再使用binarySearch查找指定值,也能够解决该问题。
使用binarySearch首先要考虑排序问题,这是咱们常常忘记的,并且在测试期间还很差发现问题,等到投入生产环境后才发现查找到的数据不许确,又是一个bug产生了,从这点看,indexOf要比binarySearch简单的多.
使用binarySearch的二分法查找比indexOf的遍历算法性能上高不少,特别是在大数据集且目标值又接近尾部时,binarySearch方法与indexOf方法相比,性能上会提高几十倍,所以从性能的角度考虑时能够选择binarySearch。