Day30 | Java集合框架之Collections工具类
Day26~29的文章我们一起学习了Java集合框架的三大核心接口:List、Set和Map。
今天我们一起看一下java.util.Collections类,注意不是之前我们提到的Collection。
Collection接口定义了集合的是什么,多了个s的Collections是一套集合操作的工具箱。
Collections里提供了很多强大、方便的静态方法,能极大简化我们对集合的操作。
一、集合排序
在实际开发中,对数据进行排序是最常见的需求之一。Collections.sort() 方法可以轻松地对任何List集合进行排序。
1.1自然排序
如果List里的元素实现了Comparable接口(如Integer, String 等),sort方法会按照它们的自然顺序进行排序。
package com.lazy.snail.day30;import java.util.ArrayList;
import java.util.Collections;
import java.util.List;/*** @ClassName Day30Demo* @Description TODO* @Author lazysnail* @Date 2025/7/7 15:13* @Version 1.0*/
public class Day30Demo {public static void main(String[] args) {List<Integer> numbers = new ArrayList<>();numbers.add(5);numbers.add(1);numbers.add(9);numbers.add(3);System.out.println("排序前: " + numbers);Collections.sort(numbers);System.out.println("排序后: " + numbers);}
}
案例中使用了Collections的sort方法,使用对象的自然排序对list进行排序。
1.2自定义排序
如果我们要对自定义对象(如Student)进行排序,sort方法还有一个接受Comparator(比较器)的版本,让我们可以随心所欲地定义排序规则。
package com.lazy.snail.day30;import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;/*** @ClassName Day30Demo2* @Description TODO* @Author lazysnail* @Date 2025/7/7 15:21* @Version 1.0*/
public class Day30Demo2 {public static void main(String[] args) {List<Student> students = new ArrayList<>();students.add(new Student("懒惰蜗牛", 28));students.add(new Student("lazysnail", 29));// 匿名内部类写法Collections.sort(students, new Comparator<Student>() {@Overridepublic int compare(Student o1, Student o2) {return o1.age - o2.age;}});// Lambda表达式Collections.sort(students, (o1, o2) -> o1.age - o2.age);// Comparator工具方法Collections.sort(students, Comparator.comparingInt(o -> o.age));System.out.println("按年龄排序后: " + students);}
}class Student {String name;int age;public Student(String name, int age) { this.name = name; this.age = age; }@Override public String toString() { return name + ":" + age; }
}
上面的案例中Comparator的创建使用了三种写法:
Java8之前传统写法,使用匿名内部类实现Comparator接口。
Java8+的Lambda表达式,这个其实是匿名内部类的语法糖。
Comparator<Student>接口是一个函数式接口(只有一个抽象方法),所以可以用Lambda替代。
Comparator.comparingInt()是Comparator提供的静态工厂方法,专门用来对int类型字段的比较。
内部封装了compare的逻辑。
其实自Java8之后,List接口自身提供了一个sort()方法。可以直接在列表对象上调用方法。
students.sort((o1, o2) -> o1.age - o2.age);
二、查找
对于一个已经排好序的List,binarySearch方法使用二分查找算法,效率要高于遍历。
使用binarySearch之前,列表必须是有序的!否则,结果就不是你想要的。
效率
package com.lazy.snail.day30;import java.util.ArrayList;
import java.util.Collections;
import java.util.List;/*** @ClassName Day30Demo3* @Description TODO* @Author lazysnail* @Date 2025/7/7 15:52* @Version 1.0*/
public class Day30Demo3 {public static void main(String[] args) {List<Integer> list = new ArrayList<>();for (int i = 0; i < 1_000_000; i++) {list.add(i);}int target = 987654;// 遍历long start1 = System.nanoTime();for (int i = 0; i < list.size(); i++) {if (list.get(i) == target) {break;}}long end1 = System.nanoTime();System.out.println("遍历查找耗时: " + (end1 - start1) / 1_000_000.0 + " ms");// binarySearchlong start2 = System.nanoTime();Collections.binarySearch(list, target);long end2 = System.nanoTime();System.out.println("binarySearch查找耗时: " + (end2 - start2) / 1_000_000.0 + " ms");}
}
从运行的结果看,二分查找的效率比普通遍历的效率高得多。
二分查找这种算法,每次都能排除掉一半的数据。相较于普通遍历的线性搜素。
在数据量很多的情况下,二分查找的时间复杂度要远远低于普通遍历。
未排序使用binarySearch
如果对没有排序的列表进行二分查找,结果就是找不到数据。
package com.lazy.snail.day30;import java.util.Arrays;
import java.util.Collections;
import java.util.List;/*** @ClassName Day30Demo4* @Description TODO* @Author lazysnail* @Date 2025/7/7 16:08* @Version 1.0*/
public class Day30Demo4 {public static void main(String[] args) {List<Integer> list = Arrays.asList(50, 20, 40, 10, 30);System.out.println("列表内容: " + list);// 想查找元素 30int index = Collections.binarySearch(list, 30);System.out.println("binarySearch 查找结果索引: " + index);}
}
三、线程安全转换
我们经常用的ArrayList、HashMap等都是非线程安全的。
在多线程环境中并发修改,可能会导致数据错乱。
Collections提供了一系列synchronizedXXX() 方法,可以把它们包装成线程安全的集合。
package com.lazy.snail.day30;import java.util.*;/*** @ClassName Day30Demo5* @Description TODO* @Author lazysnail* @Date 2025/7/7 16:26* @Version 1.0*/
public class Day30Demo5 {public static void main(String[] args) {List<String> list = new ArrayList<>();List<String> safeList = Collections.synchronizedList(list);safeList.add("懒惰蜗牛");//Set<T> safeSet = Collections.synchronizedSet(new HashSet<>());//Map<K, V> safeMap = Collections.synchronizedMap(new HashMap<>());}
}
list是线程不安全的ArrayList,通过Collections中的synchronizedList方法。
将普通的ArrayList封装成了线程安全的List。之后对list的操作就是安全的。
set、map同样有对应的方法将不安全的集合封装成线程安全的集合。
需要注意的是:这个操作只保证了单个方法(如add, get)是原子的。对于复合操作(比如“检查是否存在,如果不存在则添加”),仍然需要自己使用synchronized关键字来保证整个操作的原子性。
四、创建不可变和空集合
不可变
有时候,我们想创建一个不可变的集合,这个集合不让修改。就可以用Collections中的unmodifiableXXX()方法创建只读的集合。
package com.lazy.snail.day30;import java.util.ArrayList;
import java.util.Collections;
import java.util.List;/*** @ClassName Day30Demo6* @Description TODO* @Author lazysnail* @Date 2025/7/7 16:56* @Version 1.0*/
public class Day30Demo6 {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("懒惰蜗牛");List<String> unmodifiableList = Collections.unmodifiableList(list);//unmodifiableList.add("lazysnail");unmodifiableList.remove("懒惰蜗牛");}
}
如果想对这样的集合进行add或remove等操作,就会抛出UnsupportedOperationException异常。
从Java9开始,List, Set, Map接口自身提供了更方便的静态of()方法来直接创建真正的不可变集合。
这种方式在实际开发中更加常见。
为什么说是真正的不可变呐。回看上面的案例,稍作一下修改:
package com.lazy.snail.day30;import java.util.ArrayList;
import java.util.Collections;
import java.util.List;/*** @ClassName Day30Demo6* @Description TODO* @Author lazysnail* @Date 2025/7/7 16:56* @Version 1.0*/
public class Day30Demo6 {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("懒惰蜗牛");List<String> unmodifiableList = Collections.unmodifiableList(list);System.out.println("原集合修改前的unmodifiableList:" + unmodifiableList);list.add("lazysnail");System.out.println("原集合修改后的unmodifiableList:" + unmodifiableList);}
}
可以看出,unmodifiableList并没有改变原集合的可变性,只是加了一层只读外壳。
如果原集合一旦被外部引用持有,仍然可以修改。
而List.of(...) 是真正的不可变对象,内部结构不允许任何修改操作。
所有变更操作(add、remove、set)都会直接抛UnsupportedOperationException。
List<String> list1 = List.of("懒惰蜗牛");
list1.add("lazysnail");
"list1.add("lazysnail");"就会抛出UnsupportedOperationException。
空集合
很多返回集合的方法,如果直接返回null,保不准什么时候就会报空指针异常。
实际开发的时候,最好还是返回一个空集合。
Collections的emptyList(), emptySet(), emptyMap()就派上了用场。
package com.lazy.snail.day30;import java.util.Collections;
import java.util.List;/*** @ClassName Day30Demo7* @Description TODO* @Author lazysnail* @Date 2025/7/7 17:14* @Version 1.0*/
public class Day30Demo7 {public static List<String> findUsers(String name) {// if 找到// elsereturn Collections.emptyList();}
}
可能你会说,为什么不直接return的时候直接new ArrayList<>()。
每次return的时候,都去new一个对象,还是有开销的,能省则省。
Collections的emptyList()返回的是一个不可变的、线程安全的、可被序列化的空集合。
每次返回的都是同一个静态实例。
五、其他
Collections中还有很多其他的小工具:
reverse(List<?> list): 反转List中元素的顺序。
shuffle(List<?> list): 随机打乱List中元素的顺序。
max(Collection<?> coll) / min(Collection<?> coll): 找到集合中的最大/最小元素。
fill(List<?> list, T obj): 用指定元素替换List中的所有元素。
更多的方法参见API文档:
Collections (Java SE 17 & JDK 17)docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Collections.html
结语
Collections中有非常多关于集合的操作工具。
今天我们只是一起看了一下排序、查找、线程安全、创建不可变集合等常见的功能。
这些工具都能够在实际开发中帮我们让代码更加简洁,让集合操作更加便捷高效。
Collections里的方法不需要取死记硬背,在写代码的时候,如果涉及操作集合,
可以翻看一下API文档,如果有合适的工具,使用即可。
下一篇预告
Day31 | Lambda表达式与函数式接口
如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!
本文首发于知乎专栏