在开始学习克隆之前,我们先来看看下面的代码,普通的对象复制,存在什么问题?
class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
// 等号赋值( 基本类型)
int number = 6;
int number2 = number;
// 修改 number2 的值
number2 = 9;
System.out.println(“number:” + number);
System.out.println(“number2:” + number2);
// 等号赋值(对象)
Dog dog = new Dog();
dog.name = “旺财”;
dog.age = 5;
Dog dog2 = dog;
// 修改 dog2 的值
dog2.name = “大黄”;
dog2.age = 3;
System.out.println(dog.name + “,” + dog.age + “岁”);
System.out.println(dog2.name + “,” + dog2.age + “岁”);
}
}
程序执行结果:
number:6
number2:9
大黄,3岁
大黄,3岁
可以看出,如果使用等号复制时,对于值类型来说,彼此之间的修改操作是相对独立的,而对于引用类型来说,因为复制的是引用对象的内存地址,所以修改其中一个值,另一个值也会跟着变化,原理如下图所示:
因此为了防止这种问题的发生,就要使用对象克隆来解决引用类型复制的问题。
默认的 clone() 方法,为浅克隆,代码如下:
class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
Dog dog = new Dog();
dog.name = “旺财”;
dog.age = 5;
// 克隆
Dog dog3 = (Dog) dog.clone();
dog3.name = “小白”;
dog3.age = 2;
System.out.println(dog.name + “,” + dog.age + “岁”);
System.out.println(dog3.name + “,” + dog3.age + “岁”);
}
}
class Dog implements Cloneable {
public String name;
public int age;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
程序执行结果:
旺财,5岁
小白,2岁
可以看出使用克隆就可以解决引用类型复制的问题了,原理如下图所示:
以上这种复制方式叫做 浅克隆。
浅克隆的实现条件 :需要克隆的对象必须实现 Cloneable 接口,并重写 clone() 方法,即可实现对此对象的克隆。
然而 使用浅克隆也会存在一个问题 ,请参考以下代码。
class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
DogChild dogChild = new DogChild();
dogChild.name = “二狗”;
Dog dog4 = new Dog();
dog4.name = “大黄”;
dog4.dogChild = dogChild;
Dog dog5 = (Dog) dog4.clone();
dog5.name = “旺财”;
dog5.dogChild.name = “狗二”;
System.out.println(“dog name 4:”+dog4.name);
System.out.println(“dog name 5:”+dog5.name);
System.out.println(“dog child name 4:”+dog4.dogChild.name);
System.out.println(“dog child name 5:”+dog5.dogChild.name);
}
}
class Dog implements Cloneable {
public String name;
public DogChild dogChild;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class DogChild {
public String name;
}
程序执行结果:
dog name 4:大黄
dog name 5:旺财
dog child name 4:狗二
dog child name 5:狗二
也就是说浅克隆,只会复制对象的值类型,而不会复制对象的引用类型。原因如下图所示:
要处理引用类型不被复制的问题,就要使用到 深克隆 。
定义 :深克隆就是复制整个对象信息,包含值类型和引用类型。
深克隆的实现方式 通常包含以下两种。
深克隆实现方式一:序列化
实现思路:先将要拷贝对象写入到内存中的字节流中,然后再从这个字节流中读出刚刚存储的信息,作为一个新对象返回,那么这个新对象和原对象就不存在任何地址上的共享,自然实现了深拷贝。请参考以下代码:
class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
BirdChild birdChild = new BirdChild();
birdChild.name = “小小鸟”;
Bird bird = new Bird();
bird.name = “小鸟”;
bird.birdChild = birdChild;
// 使用序列化克隆对象
Bird bird2 = CloneUtils.clone(bird);
bird2.name = “黄雀”;
bird2.birdChild.name = “小黄雀”;
System.out.println(“bird name:” + bird.name);
System.out.println(“bird child name:” + bird.birdChild.name);
System.out.println(“bird name 2:” + bird2.name);
System.out.println(“bird child name 2:” + bird2.birdChild.name);
}
}
class CloneUtils {
public static T clone(T obj) {
T cloneObj = null;
try {
//写入字节流
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bo);
oos.writeObject(obj);
oos.close();
//分配内存,写入原始对象,生成新对象
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());//获取上面的输出字节流
ObjectInputStream oi = new ObjectInputStream(bi);
//返回生成的新对象
cloneObj = (T) oi.readObject();
oi.close();
} catch (Exception e) {
e.printStackTrace();
}
return cloneObj;
}
}
程序执行结果:
bird name:小鸟
bird child name:小小鸟
bird name 2:黄雀
bird child name 2:小黄雀
深克隆实现方式二:所有引用类型都实现克隆
class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ParrotChild parrotChild = new ParrotChild();
parrotChild.name = “小鹦鹉”;
Parrot parrot = new Parrot();
parrot.name = “大鹦鹉”;
parrot.parrotChild = parrotChild;
// 克隆
Parrot parrot2 = (Parrot) parrot.clone();
parrot2.name = “老鹦鹉”;
parrot2.parrotChild.name = “少鹦鹉”;
System.out.println(“parrot name:” + parrot.name);
System.out.println(“parrot child name:” + parrot.parrotChild.name);
System.out.println(“parrot name 2:” + parrot2.name);
System.out.println(“parrot child name 2:” + parrot2.parrotChild.name);
}
}
class Parrot implements Cloneable {
public String name;
public ParrotChild parrotChild;
@Override
protected Object clone() throws CloneNotSupportedException {
Parrot bird = (Parrot) super.clone();
bird.parrotChild = (ParrotChild) parrotChild.clone();
return bird;
}
}
class ParrotChild implements Cloneable {
public String name;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
程序执行结果:
parrot name:大鹦鹉
parrot child name:小鹦鹉
parrot name 2:老鹦鹉
parrot child name 2:少鹦鹉
内存中的数据对象只有转换成二进制流才能进行数据持久化或者网络传输,将对象转换成二进制流的过程叫做序列化(Serialization);相反,把二进制流恢复为数据对象的过程就称之为反序列化(Deserialization)。
先把对象序列化到磁盘,再从磁盘中反序列化出对象,请参考以下代码:
class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 对象赋值
User user = new User();
user.setName(“老王”);
user.setAge(30);
System.out.println(user);
// 创建输出流(序列化内容到磁盘)
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(“test.out”));
// 序列化对象
oos.writeObject(user);
oos.flush();
oos.close();
// 创建输入流(从磁盘反序列化)
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(“test.out”));
// 反序列化
User user2 = (User) ois.readObject();
ois.close();
System.out.println(user2);
}
}
class User implements Serializable {
private static final long serialVersionUID = 3831264392873197003L;
private String name;
private int age;
@Override
public String toString() {
return “{name:” + name + “,age:” + age + “}”;
}
// setter/getter…
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
程序执行结果:
{name:老王,age:30}
{name:老王,age:30}
更多序列化和反序列化的实现方式以及代码示例,请看下文面试部分的内容。
开发工具设置 :IDEA 开启自动生成 serialVersionUID
点击 Settings → Inspections → 搜索 Serialization issues → 勾选 Serializable class
without ‘SerialVersionUID’ 保存设置,如下图所示:
设置完之后,光标放到类名上,点击提示,生成 serialVersionUID,如下图所示:
答:如果显示定义了 serialVersionUID 值之后,可以使序列化和反序列化向后兼容。也就是说如果 serialVersionUID
的值相同,修改对象的字段(删除或增加),程序不会报错,之后给没有的字段赋值为 null,而如果没有指定 serialVersionUID
的值,如果修改对象的字段,程序就会报错。如下图所示:
答:可序列化 Serializalbe 接口存在于 java.io 包中,构成了 Java
序列化机制的核心,它没有任何方法,它的用途是标记某对象为可序列化对象,指示编译器使用 Java 序列化机制序列化此对象。
答:常用的序列化有以下三种方式:
1)Java 原生序列化方式
请参考以下代码:
// 序列化和反序列化
class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 对象赋值
User user = new User();
user.setName(“老王”);
user.setAge(30);
System.out.println(user);
// 创建输出流(序列化内容到磁盘)
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(“test.out”));
// 序列化对象
oos.writeObject(user);
oos.flush();
oos.close();
// 创建输入流(从磁盘反序列化)
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(“test.out”));
// 反序列化
User user2 = (User) ois.readObject();
ois.close();
System.out.println(user2);
}
}
class User implements Serializable {
private static final long serialVersionUID = 5132320539584511249L;
private String name;
private int age;
@Override
public String toString() {
return “{name:” + name + “,age:” + age + “}”;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
2)JSON 格式,可使用 fastjson 或 GSON
JSON 是一种轻量级的数据格式,JSON 序列化的优点是可读性比较高,方便调试。我们本篇以 fastjson 的序列化为例,请参考以下代码:
// 序列化和反序列化
class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 对象赋值
User user = new User();
user.setName(“老王”);
user.setAge(30);
System.out.println(user);
String jsonSerialize = JSON.toJSONString(user);
User user3 = (User) JSON.parseObject(jsonSerialize, User.class);
System.out.println(user3);
}
}
class User implements Serializable {
private static final long serialVersionUID = 5132320539584511249L;
private String name;
private int age;
@Override
public String toString() {
return "{name:" + name + ",age:" + age + "}";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
3)Hessian 方式序列化
Hessian 序列化的优点是可以跨编程语言,比 Java 原生的序列化和反序列化效率高。
请参考以下示例代码:
// 序列化和反序列化
class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 序列化
ByteArrayOutputStream bo = new ByteArrayOutputStream();
HessianOutput hessianOutput = new HessianOutput(bo);
hessianOutput.writeObject(user);
byte[] hessianBytes = bo.toByteArray();
// 反序列化
ByteArrayInputStream bi = new ByteArrayInputStream(hessianBytes);
HessianInput hessianInput = new HessianInput(bi);
User user4 = (User) hessianInput.readObject();
System.out.println(user4);
}
}
class User implements Serializable {
private static final long serialVersionUID = 5132320539584511249L;
private String name;
private int age;
@Override
public String toString() {
return “{name:” + name + “,age:” + age + “}”;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
答:好处包含以下几点。
clone() 源代码,如下图:
答:区别主要在对引用类型的复制上,具体信息如下。
答:克隆的对象实现 Cloneable 接口,并重写 clone() 方法就可以实现浅克隆了。
import java.util.Arrays;
class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
CloneObj cloneObj = new CloneObj();
cloneObj.name = “老王”;
cloneObj.age = 30;
cloneObj.sistersAge = new int[]{18, 19};
CloneObj cloneObj2 = (CloneObj) cloneObj.clone();
cloneObj2.name = “磊哥”;
cloneObj2.age = 33;
cloneObj2.sistersAge[0] = 20;
System.out.println(cloneObj.name + “|” + cloneObj2.name);
System.out.println(cloneObj.age + “|” + cloneObj2.age);
System.out.println(Arrays.toString(cloneObj.sistersAge) + “|” + Arrays.toString(cloneObj2.sistersAge));
}
}
class CloneObj implements Cloneable {
public String name;
public int age;
public int[] sistersAge;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
答:执行结果如下。
老王|磊哥
30|33
[20, 19]|[20, 19]
答:一般实现方式有两种。
答:虽然所有类都是 Object 的子类,但因为 Object 中的 clone() 方法被声明为 protected 访问级别,所以非 java.lang
包下的其他类是不能直接使用的。因此要想实现克隆功能,就必须实现 Cloneable,并重写 clone() 方法才行。
答:先将原对象序列化到内存的字节流中,再从字节流中反序列化出刚刚存储的对象,这个新对象和原对象就不存在任何地址上的共享,这样就实现了深克隆。
答:可以把不需要序列化的成员设置为瞬态(trasient)和静态变量,这样就不会被序列化了,瞬态的使用如下:
public transient int num;
答:可以,在 Java 中默认序列化一个对象需要调用 ObjectOutputStream.writeObject(saveThisObject) 和
ObjectInputStream.readObject()
读取对象,你可以自定义这两个方法,从而实现自定义序列化的过程。需要注意的重要一点是,记得声明这些方法为私有方法,以避免被继承、重写或重载。
答:在 Java 中序列化由 java.io.ObjectOutputStream
类完成,该类是一个筛选器流,它封装在较低级别的字节流中,以处理序列化机制。要通过序列化机制存储任何对象,我们需要调用
ObjectOutputStream.writeObject(savethisobject) 方法,如果要反序列化该对象,我们需要调用
ObjectInputStream.readObject() 方法,readObject() 方法会读取字节,并把这些字节转换为对象再返回。
序列化常见的使用场景是远程服务调用(RPC)和网络对象传输等,可通过 implements Serializable
来实现对象序列化,在序列化对象中通过定义 serialVersionUID 来防止执行不兼容的类更改。调用 Object 类中的 clone()
方法默认是浅克隆,浅克隆只能复制值类型,不能复制引用类型,因此更多的时候我们需要深克隆,深克隆通常的实现方式有两种:序列化和所有引用类型都实现克隆。
先来看看集合的继承关系图,如下图所示:
其中:
为了方便理解,我隐藏了一些与本文内容无关的信息,隐藏的这些内容会在后面的章节中进行详细地介绍。
从图中可以看出,集合的根节点是 Collection,而 Collection 下又提供了两大常用集合,分别是:
下面我们分别对集合类进行详细地介绍。
Vector 是 Java 早期提供的线程安全的有序集合,如果不需要线程安全,不建议使用此集合,毕竟同步是有线程开销的。
使用示例代码:
Vector vector = new Vector();
vector.add("dog");
vector.add("cat");
vector.remove("cat");
System.out.println(vector);
程序执行结果:[dog]
ArrayList
是最常见的非线程安全的有序集合,因为内部是数组存储的,所以随机访问效率很高,但非尾部的插入和删除性能较低,如果在中间插入元素,之后的所有元素都要后移。ArrayList
的使用与 Vector 类似。
LinkedList 是使用双向链表数据结构实现的,因此增加和删除效率比较高,而随机访问效率较差。
LinkedList 除了包含以上两个类的操作方法之外,还新增了几个操作方法,如 offer() 、peek() 等,具体详情,请参考以下代码:
LinkedList linkedList = new LinkedList();
// 添加元素
linkedList.offer("bird");
linkedList.push("cat");
linkedList.push("dog");
// 获取第一个元素
System.out.println(linkedList.peek());
// 获取第一个元素,并删除此元素
System.out.println(linkedList.poll());
System.out.println(linkedList);
程序的执行结果:
dog
dog
[cat, bird]
HashSet 是一个没有重复元素的集合。虽然它是 Set 集合的子类,实际却为 HashMap 的实例,相关源码如下:
public HashSet() {
map = new HashMap<>();
}
因此 HashSet 是无序集合,没有办法保证元素的顺序性。
HashSet 默认容量为 16,每次扩充 0.75 倍,相关源码如下:
public HashSet(Collection extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
HashSet 的使用与 Vector 类似。
TreeSet 集合实现了自动排序,也就是说 TreeSet 会把你插入数据进行自动排序。
示例代码如下:
TreeSet treeSet = new TreeSet();
treeSet.add("dog");
treeSet.add("camel");
treeSet.add("cat");
treeSet.add("ant");
System.out.println(treeSet);
程序执行结果:[ant, camel, cat, dog]
可以看出,TreeSet 的使用与 Vector 类似,只是实现了自动排序。
LinkedHashSet 是按照元素的 hashCode
值来决定元素的存储位置,但同时又使用链表来维护元素的次序,这样使得它看起来像是按照插入顺序保存的。
LinkedHashSet 的使用与 Vector 类似。
集合和数组的转换可使用 toArray() 和 Arrays.asList() 来实现,请参考以下代码示例:
List list = new ArrayList();
list.add("cat");
list.add("dog");
// 集合转数组
String[] arr = list.toArray(new String[list.size()]);
// 数组转集合
List list2 = Arrays.asList(arr);
集合与数组的区别,可以参考「数组和排序算法的应用 +
面试题」的内容。
在 Java 语言中排序提供了两种方式:Comparable 和 Comparator,它们的区别也是常见的面试题之一。下面我们彻底地来了解一下
Comparable 和 Comparator 的使用与区别。
Comparable 位于 java.lang 包下,是一个排序接口,也就是说如果一个类实现了 Comparable 接口,就意味着该类有了排序功能。
Comparable 接口只包含了一个函数,定义如下:
package java.lang;
import java.util.*;
public interface Comparable {
public int compareTo(T o);
}
Comparable 使用示例 ,请参考以下代码:
class ComparableTest {
public static void main(String[] args) {
Dog[] dogs = new Dog[]{
new Dog("老旺财", 10),
new Dog("小旺财", 3),
new Dog("二旺财", 5),
};
// Comparable 排序
Arrays.sort(dogs);
for (Dog d : dogs) {
System.out.println(d.getName() + ":" + d.getAge());
}
}
}
class Dog implements Comparable {
private String name;
private int age;
@Override
public int compareTo(Dog o) {
return age - o.age;
}
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
程序执行结果:
小旺财:3
二旺财:5
老旺财:10
如果 Dog 类未实现 Comparable 执行代码会报程序异常的信息,错误信息为:
Exception in thread “main” java.lang.ClassCastException: xxx cannot be cast
to java.lang.Comparable
compareTo() 返回值有三种:
Comparator 是一个外部比较器,位于 java.util 包下,之所以说 Comparator 是一个外部比较器,是因为它无需在比较类中实现
Comparator 接口,而是要新创建一个比较器类来进行比较和排序。
Comparator 接口包含的主要方法为 compare(),定义如下:
public interface Comparator {
int compare(T o1, T o2);
}
Comparator 使用示例 ,请参考以下代码:
class ComparatorTest {
public static void main(String[] args) {
Dog[] dogs = new Dog[]{
new Dog("老旺财", 10),
new Dog("小旺财", 3),
new Dog("二旺财", 5),
};
// Comparator 排序
Arrays.sort(dogs,new DogComparator());
for (Dog d : dogs) {
System.out.println(d.getName() + ":" + d.getAge());
}
}
}
class DogComparator implements Comparator {
@Override
public int compare(Dog o1, Dog o2) {
return o1.getAge() - o2.getAge();
}
}
class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
程序执行结果:
小旺财:3
二旺财:5
老旺财:10
答:区别分为以下几个方面:
答:TreeSet 集合实现了元素的自动排序,也就是说无需任何操作,即可实现元素的自动排序功能。
答:Vector 和 ArrayList 的默认容量都为 10,源码如下。
Vector 默认容量源码:
public Vector() {
this(10);
}
ArrayList 默认容量源码:
private static final int DEFAULT_CAPACITY = 10;
Vector 容量扩充默认增加 1 倍,源码如下:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
其中 capacityIncrement 为初始化 Vector 指定的,默认情况为 0。
ArrayList 容量扩充默认增加大概 0.5 倍(oldCapacity + (oldCapacity >> 1)),源码如下(JDK 8):
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
答:这三者都是 List 的子类,因此功能比较相似,比如增加和删除操作、查找元素等,但在性能、线程安全等方面表现却又不相同,差异如下:
答:Vector 和 ArrayList
的内部结构是以数组形式存储的,因此非常适合随机访问,但非尾部的删除或新增性能较差,比如我们在中间插入一个元素,就需要把后续的所有元素都进行移动。
LinkedList 插入和删除元素效率比较高,但随机访问性能会比以上两个动态数组慢。
答:Collection 和 Collections 的区别如下:
A:List
B:Set
C:Map
D:HashSet
答:C
答:LinkedHashSet 底层数据结构由哈希表和链表组成,链表保证了元素的有序即存储和取出一致,哈希表保证了元素的唯一性。
答:HashSet 的底层其实就是 HashMap,只不过 HashSet 实现了 Set 接口并且把数据作为 K 值,而 V
值一直使用一个相同的虚值来保存,我们可以看到源码:
public boolean add(E e) {
return map.put(e, PRESENT)==null;// 调用 HashMap 的 put 方法,PRESENT 是一个至始至终都相同的虚值
}
由于 HashMap 的 K 值本身就不允许重复,并且在 HashMap 中如果 K/V 相同时,会用新的 V 覆盖掉旧的 V,然后返回旧的 V,那么在
HashSet 中执行这一句话始终会返回一个 false,导致插入失败,这样就保证了数据的不可重复性。
Integer num = 10;
Integer num2 = 5;
System.out.println(num.compareTo(num2));
答:程序输出的结果是 1
,因为 Integer 默认实现了 compareTo 方法,定义了自然排序规则,所以当 num 比 num2 大时会返回
1,Integer 相关源码如下:
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
答:可以使用集合中的 Stack 实现,Stack 是标准的后进先出的栈结构,使用 Stack 中的 pop()
方法返回栈顶元素并删除该元素,示例代码如下。
Stack stack = new Stack();
stack.push("a");
stack.push("b");
stack.push("c");
for (int i = 0; i < 3; i++) {
// 移除并返回栈顶元素
System.out.print(stack.pop() + " ");
}
程序执行结果:c b a
答:peek() 方法返回第一个元素,但不删除当前元素,当元素不存在时返回 null;poll() 方法返回第一个元素并删除此元素,当元素不存在时返回
null。
答:Comparable 和 Comparator 的主要区别如下:
本文介绍的集合都实现自 Collection,因此它们都有同样的操作方法,如 add()、addAll()、remove() 等,Collection
接口的方法列表如下图:
当然部分集合也在原有方法上扩充了自己特有的方法,如 LinkedList 的 offer()、push()
等方法。本文也提供了数组和集合互转方法,List.toArray() 把集合转换为数组,Arrays.asList(array)
把数组转换为集合。最后介绍了 Comparable 和 Comparator 的使用和区别,Comparable 和 Comparator 是 Java
语言排序提供的两种排序方式,Comparable 位于 java.lang 包下,如果一个类实现了 Comparable 接口,就意味着该类有了排序功能;而
Comparator 位于 java.util 包下,是一个外部比较器,它无需在比较类中实现 Comparator
接口,而是要新创建一个比较器类来进行比较和排序。
集合有两个大接口:Collection 和 Map,本文重点来讲解集合中另一个常用的集合类型 Map。
以下是 Map 的继承关系图:
Map 常用的实现类如下:
常用方法包括:put、remove、get、size 等,所有方法如下图:
使用示例,请参考以下代码:
Map hashMap = new HashMap();
// 增加元素
hashMap.put("name", "老王");
hashMap.put("age", "30");
hashMap.put("sex", "你猜");
// 删除元素
hashMap.remove("age");
// 查找单个元素
System.out.println(hashMap.get("age"));
// 循环所有的 key
for (Object k : hashMap.keySet()) {
System.out.println(k);
}
// 循环所有的值
for (Object v : hashMap.values()) {
System.out.println(v);
}
以上为 HashMap 的使用示例,其他类的使用也是类似。
HashMap 底层的数据是数组被成为哈希桶,每个桶存放的是链表,链表中的每个节点,就是 HashMap 中的每个元素。在 JDK 8 当链表长度大于等于
8 时,就会转成红黑树的数据结构,以提升查询和插入的效率。
HashMap 数据结构,如下图:
执行流程如下:
源码及说明:
public V put(K key, V value) {
// 对 key 进行 hash()
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
// 对 key 进行 hash() 的具体实现
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
// tab为空则创建
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算 index,并对 null 做处理
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
// 节点存在
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 该链为树
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
// 该链为链表
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 写入
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 超过load factor*current capacity,resize
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
put() 执行流程图如下:
执行流程如下:
源码及说明:
public V get(Object key) {
Node e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 该方法是 Map.get 方法的具体实现
* 接收两个参数
* @param hash key 的 hash 值,根据 hash 值在节点数组中寻址,该 hash 值是通过 hash(key) 得到的
* @param key key 对象,当存在 hash 碰撞时,要逐个比对是否相等
* @return 查找到则返回键值对节点对象,否则返回 null
*/
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k; // 声明节点数组对象、链表的第一个节点对象、循环遍历时的当前节点对象、数组长度、节点的键对象
// 节点数组赋值、数组长度赋值、通过位运算得到求模结果确定链表的首节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // 首先比对首节点,如果首节点的 hash 值和 key 的 hash 值相同,并且首节点的键对象和 key 相同(地址相同或 equals 相等),则返回该节点
((k = first.key) == key || (key != null && key.equals(k))))
return first; // 返回首节点
// 如果首节点比对不相同、那么看看是否存在下一个节点,如果存在的话,可以继续比对,如果不存在就意味着 key 没有匹配的键值对
if ((e = first.next) != null) {
// 如果存在下一个节点 e,那么先看看这个首节点是否是个树节点
if (first instanceof TreeNode)
// 如果是首节点是树节点,那么遍历树来查找
return ((TreeNode)first).getTreeNode(hash, key);
// 如果首节点不是树节点,就说明还是个普通的链表,那么逐个遍历比对即可
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) // 比对时还是先看 hash 值是否相同、再看地址或 equals
return e; // 如果当前节点e的键对象和key相同,那么返回 e
} while ((e = e.next) != null); // 看看是否还有下一个节点,如果有,继续下一轮比对,否则跳出循环
}
}
return null; // 在比对完了应该比对的树节点 或者全部的链表节点 都没能匹配到 key,那么就返回 null
答:Map 的常见实现类如下列表:
答:HashMap 在并发场景中可能出现死循环的问题,这是因为 HashMap
在扩容的时候会对链表进行一次倒序处理,假设两个线程同时执行扩容操作,第一个线程正在执行 B→A 的时候,第二个线程又执行了 A→B ,这个时候就会出现
B→A→B 的问题,造成死循环。
解决的方法:升级 JDK 版本,在 JDK 8 之后扩容不会再进行倒序,因此死循环的问题得到了极大的改善,但这不是终极的方案,因为 HashMap
本来就不是用在多线程版本下的,如果是多线程可使用 ConcurrentHashMap 替代 HashMap。
A:Hashtable 和 HashMap 都是非线程安全的
B:ConcurrentHashMap 允许 null 作为 key
C:HashMap 允许 null 作为 key
D:Hashtable 允许 null 作为 key
答:C
题目解析:Hashtable 是线程安全的,ConcurrentHashMap 和 Hashtable 是不允许 null 作为键和值的。
答:使用 Collections.sort(list, new Comparator
自定义比较器实现,先把 TreeMap 转换为 ArrayList,在使用 Collections.sort() 根据 value
进行倒序,完整的实现代码如下。
TreeMap treeMap = new TreeMap();
treeMap.put("dog", "dog");
treeMap.put("camel", "camel");
treeMap.put("cat", "cat");
treeMap.put("ant", "ant");
// map.entrySet() 转成 List
List> list = new ArrayList<>(treeMap.entrySet());
// 通过比较器实现比较排序
Collections.sort(list, new Comparator>() {
public int compare(Map.Entry m1, Map.Entry m2) {
return m2.getValue().compareTo(m1.getValue());
}
});
// 打印结果
for (Map.Entry item : list) {
System.out.println(item.getKey() + ":" + item.getValue());
}
程序执行结果:
dog:dog
cat:cat
camel:camel
ant:ant
A:LinedHashSet
B:HashSet
C:TreeSet
D:AbstractSet
答:C
Hashtable hashtable = new Hashtable();
hashtable.put("table", null);
System.out.println(hashtable.get("table"));
答:程序执行报错:java.lang.NullPointerException。Hashtable 不允许 null 键和值。
答:HashMap 有两个重要的参数:容量(Capacity)和负载因子(LoadFactor)。
答:HashMap 和 Hashtable 区别如下:
答:当输入两个不同值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。
答:哈希冲突的常用解决方案有以下 4 种。
答:HashMap 使用链表和红黑树来解决哈希冲突,详见本文 put() 方法的执行过程。
答:这样做的目的是为了让散列更加均匀,从而减少哈希碰撞,以提供代码的执行效率。
答:如果有哈希冲突,HashMap 会循环链表中的每项 key 进行 equals 对比,返回对应的元素。相关源码如下:
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) // 比对时还是先看 hash 值是否相同、再看地址或 equals
return e; // 如果当前节点 e 的键对象和 key 相同,那么返回 e
} while ((e = e.next) != null); // 看看是否还有下一个节点,如果有,继续下一轮比对,否则跳出循环
class Person {
private Integer age;
public boolean equals(Object o) {
if (o == null || !(o instanceof Person)) {
return false;
} else {
return this.getAge().equals(((Person) o).getAge());
}
}
public int hashCode() {
return age.hashCode();
}
public Person(int age) {
this.age = age;
}
public void setAge(int age) {
this.age = age;
}
public Integer getAge() {
return age;
}
public static void main(String[] args) {
HashMap hashMap = new HashMap<>();
Person person = new Person(18);
hashMap.put(person, 1);
System.out.println(hashMap.get(new Person(18)));
}
}
答:1
题目解析:因为 Person 重写了 equals 和 hashCode 方法,所有 person 对象和 new Person(18)
的键值相同,所以结果就是 1。
答:因为 Java 规定,如果两个对象 equals 比较相等(结果为 true),那么调用 hashCode 也必须相等。如果重写了 equals()
但没有重写 hashCode(),就会与规定相违背,比如以下代码(故意注释掉 hashCode 方法):
class Person {
private Integer age;
public boolean equals(Object o) {
if (o == null || !(o instanceof Person)) {
return false;
} else {
return this.getAge().equals(((Person) o).getAge());
}
}
// public int hashCode() {
// return age.hashCode();
// }
public Person(int age) {
this.age = age;
}
public void setAge(int age) {
this.age = age;
}
public Integer getAge() {
return age;
}
public static void main(String[] args) {
Person p1 = new Person(18);
Person p2 = new Person(18);
System.out.println(p1.equals(p2));
System.out.println(p1.hashCode() + " : " + p2.hashCode());
}
}
执行的结果:
true
21685669 : 2133927002
如果重写 hashCode() 之后,执行的结果是:
true
18 : 18
这样就符合了 Java 的规定,因此重写 equals() 时一定要重写 hashCode()。
答:HashMap 在 JDK 7 中会导致死循环的问题。因为在 JDK 7 中,多线程进行 HashMap 扩容时会导致链表的循环引用,这个时候使用
get() 获取元素时就会导致死循环,造成 CPU 100% 的情况。
答:HashMap 在 JDK 7 和 JDK 8 的主要区别如下。
通过本文可以了解到:
HashMap 在 JDK 7 可能在扩容时会导致链表的循环引用而造成 CPU 100%,HashMap 在 JDK 8 时数据结构变更为:数组 + 链表
在泛型没有诞生之前,我们经常会遇到这样的问题,如以下代码所示:
ArrayList arrayList = new ArrayList();
arrayList.add("Java");
arrayList.add(24);
for (int i = 0; i < arrayList.size(); i++) {
String str = (String) arrayList.get(i);
System.out.println(str);
}
看起来好像没有什么大问题,也能正常编译,但真正运行起来就会报错:
Exception in thread “main” java.lang.ClassCastException: java.lang.Integer
cannot be cast to java.lang.String
at xxx(xxx.java:12)
类型转换出错,当我们给 ArrayList
放入不同类型的数据,却使用一种类型进行接收的时候,就会出现很多类似的错误,可能更多的时候,是因为开发人员的不小心导致的。那有没有好的办法可以杜绝此类问题的发生呢?这个时候
Java 语言提供了一个很好的解决方案——“泛型”。
泛型 :泛型本质上是类型参数化,解决了不确定对象的类型问题。
泛型的使用,请参考以下代码:
ArrayList arrayList = new ArrayList();
arrayList.add("Java");
这个时候如果给 arrayList 添加非 String 类型的元素,编译器就会报错,提醒开发人员插入相同类型的元素。
报错信息如下图所示:
这样就可以避免开头示例中,类型不一致导致程序运行过程中报错的问题了。
泛型的优点主要体现在以下三个方面。
我们回想一下,在迭代器(Iterator)没有出现之前,如果要遍历数组和集合,需要使用方法。
数组遍历,代码如下:
String[] arr = new String[]{"Java", "Java虚拟机", "Java中文社群"};
for (int i = 0; i < arr.length; i++) {
String item = arr[i];
}
集合遍历,代码如下:
List list = new ArrayList() {{
add("Java");
add("Java虚拟机");
add("Java中文社群");
}};
for (int i = 0; i < list.size(); i++) {
String item = list.get(i);
}
而迭代器的产生,就是为不同类型的容器遍历,提供标准统一的方法。
迭代器遍历,代码如下:
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object object = iterator.next();
// do something
}
总结 :使用了迭代器就可以不用关注容器的内部细节,用同样的方式遍历不同类型的容器。
迭代器是用来遍历容器内所有元素对象的,也是一种常见的设计模式。
迭代器包含以下四个方法。
迭代器使用如下:
List list = new ArrayList() {{
add("Java");
add("Java虚拟机");
add("Java中文社群");
}};
Iterator iterator = list.iterator();
// 遍历
while (iterator.hasNext()){
String str = (String) iterator.next();
if (str.equals("Java中文社群")){
iterator.remove();
}
}
System.out.println(list);
程序执行结果:
[Java, Java虚拟机]
forEachRemaining 使用如下:
List list = new ArrayList() {{
add("Java");
add("Java虚拟机");
add("Java中文社群");
}};
// forEachRemaining 使用
list.iterator().forEachRemaining(item -> System.out.println(item));
答:因为迭代器不需要关注容器的内部细节,所以 next() 返回 Object 类型就可以接收任何类型的对象。
答:HashMap 的遍历分为以下四种方式。
以上方式的代码实现如下:
Map hashMap = new HashMap();
hashMap.put("name", "老王");
hashMap.put("sex", "你猜");
// 方式一:entrySet 遍历
for (Map.Entry item : hashMap.entrySet()) {
System.out.println(item.getKey() + ":" + item.getValue());
}
// 方式二:iterator 遍历
Iterator> iterator = hashMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry entry = iterator.next();
System.out.println(entry.getKey() + ":" + entry.getValue());
}
// 方式三:遍历所有的 key 和 value
for (Object k : hashMap.keySet()) {
// 循环所有的 key
System.out.println(k);
}
for (Object v : hashMap.values()) {
// 循环所有的值
System.out.println(v);
}
// 方式四:通过 key 值遍历
for (Object k : hashMap.keySet()) {
System.out.println(k + ":" + hashMap.get(k));
}
A:泛型可以修饰类
B:泛型可以修饰方法
C:泛型不可以修饰接口
D:以上说法全错
答:选 C,泛型可以修饰类、方法、接口、变量。
例如:
public interface Iterable {
}
List list = new ArrayList<>();
List list2 = new ArrayList<>();
System.out.println(list.getClass() == list2.getClass());
答:程序的执行结果是 true
。
题目解析:Java 中泛型在编译时会进行类型擦除,因此 List
和 List
类型擦除后的结果都是 java.util.ArrayLis ,进而 list.getClass() == list2.getClass() 的结果也一定是
true。
List
和 List>
有什么区别?答:List>
可以容纳任意类型,只不过 List>
被赋值之后,就不允许添加和修改操作了;而 List
和
List>
不同的是它在赋值之后,可以进行添加和修改操作,如下图所示:
List
赋值给 List
吗?答:不可以,编译器会报错,如下图所示:
List
和 List
的区别是什么?答: List
和 List
都能存储任意类型的数据,但 List
和 List
的唯一区别就是,List
不会触发编译器的类型安全检查,比如把 List
赋值给 List
是没有任何问题的,但赋值给
List
就不行,如下图所示:
List list = new ArrayList<>();
list.add("Java");
list.add("Java虚拟机");
list.add("Java中文社群");
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
String str = (String) iterator.next();
if (str.equals("Java中文社群")) {
iterator.remove();
}
}
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
System.out.println("Over");
答:程序打印结果是 Over
。
题目解析:因为第一个 while 循环之后,iterator.hasNext() 返回值就为 false 了,所以不会进入第二个循环,之后打印最后的
Over。
答:泛型是通过类型擦除来实现的,类型擦除指的是编译器在编译时,会擦除了所有类型相关的信息,比如 List
在编译后就会变成 List
类型,这样做的目的就是确保能和 Java 5 之前的版本(二进制类库)进行兼容。
通过本文知道了泛型的优点:安全性、避免类型转换、提高了代码的可读性。泛型的本质是类型参数化,但编译之后会执行类型擦除,这样就可以和 Java 5
之前的二进制类库进行兼容。本文也介绍了迭代器(Iterator)的使用,使用迭代器的好处是不用关注容器的内部细节,用同样的方式遍历不同类型的容器。
队列(Queue):与栈相对的一种数据结构,
集合(Collection)的一个子类。队列允许在一端进行插入操作,而在另一端进行删除操作的线性表,栈的特点是后进先出,而队列的特点是先进先出。队列的用处很大,比如实现消息队列。
Queue 类关系图,如下图所示:
注:为了让读者更直观地理解,上图为精简版的 Queue 类关系图。本文如无特殊说明,内容都是基于 Java 1.8 版本。
从上图可以看出 Queue 大体可分为以下三类。
Queue 常用方法,如下图所示:
方法说明:
Queue linkedList = new LinkedList<>();
linkedList.add("Dog");
linkedList.add("Camel");
linkedList.add("Cat");
while (!linkedList.isEmpty()) {
System.out.println(linkedList.poll());
}
程序执行结果:
Dog
Camel
Cat
BlockingQueue 在 java.util.concurrent 包下,其他阻塞类都实现自 BlockingQueue
接口,BlockingQueue
提供了线程安全的队列访问方式,当向队列中插入数据时,如果队列已满,线程则会阻塞等待队列中元素被取出后再插入;当从队列中取数据时,如果队列为空,则线程会阻塞等待队列中有新元素再获取。
BlockingQueue 核心方法
插入方法:
LinkedBlockingQueue 是一个由链表实现的有界阻塞队列,容量默认值为
Integer.MAX_VALUE,也可以自定义容量,建议指定容量大小,默认大小在添加速度大于删除速度情况下有造成内存溢出的风险,LinkedBlockingQueue
是先进先出的方式存储元素。
ArrayBlockingQueue
是一个有边界的阻塞队列,它的内部实现是一个数组。它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。
ArrayBlockingQueue 也是先进先出的方式存储数据,ArrayBlockingQueue 内部的阻塞队列是通过重入锁 ReenterLock
和 Condition 条件队列实现的,因此 ArrayBlockingQueue
中的元素存在公平访问与非公平访问的区别,对于公平访问队列,被阻塞的线程可以按照阻塞的先后顺序访问队列,即先阻塞的线程先访问队列。而非公平队列,当队列可用时,阻塞的线程将进入争夺访问资源的竞争中,也就是说谁先抢到谁就执行,没有固定的先后顺序。
示例代码如下:
// 默认非公平阻塞队列
ArrayBlockingQueue queue = new ArrayBlockingQueue(6);
// 公平阻塞队列
ArrayBlockingQueue queue2 = new ArrayBlockingQueue(6,true);
// ArrayBlockingQueue 源码展示
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
DelayQueue 是一个支持延时获取元素的无界阻塞队列,队列中的元素必须实现 Delayed
接口,在创建元素时可以指定延迟时间,只有到达了延迟的时间之后,才能获取到该元素。
实现了 Delayed 接口必须重写两个方法 ,getDelay(TimeUnit) 和 compareTo(Delayed),如下代码所示:
class DelayElement implements Delayed {
@Override
// 获取剩余时间
public long getDelay(TimeUnit unit) {
// do something
}
@Override
// 队列里元素的排序依据
public int compareTo(Delayed o) {
// do something
}
}
DelayQueue 使用的完整示例 ,请参考以下代码:
public class DelayTest {
public static void main(String[] args) throws InterruptedException {
DelayQueue delayQueue = new DelayQueue();
delayQueue.put(new DelayElement(1000));
delayQueue.put(new DelayElement(3000));
delayQueue.put(new DelayElement(5000));
System.out.println("开始时间:" + DateFormat.getDateTimeInstance().format(new Date()));
while (!delayQueue.isEmpty()){
System.out.println(delayQueue.take());
}
System.out.println("结束时间:" + DateFormat.getDateTimeInstance().format(new Date()));
}
static class DelayElement implements Delayed {
// 延迟截止时间(单面:毫秒)
long delayTime = System.currentTimeMillis();
public DelayElement(long delayTime) {
this.delayTime = (this.delayTime + delayTime);
}
@Override
// 获取剩余时间
public long getDelay(TimeUnit unit) {
return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
// 队列里元素的排序依据
public int compareTo(Delayed o) {
if (this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS)) {
return 1;
} else if (this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS)) {
return -1;
} else {
return 0;
}
}
@Override
public String toString() {
return DateFormat.getDateTimeInstance().format(new Date(delayTime));
}
}
}
程序执行结果:
开始时间:2019-6-13 20:40:38
2019-6-13 20:40:39
2019-6-13 20:40:41
2019-6-13 20:40:43
结束时间:2019-6-13 20:40:43
ConcurrentLinkedQueue
是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。
它的入队和出队操作均利用 CAS(Compare And Set)更新,这样允许多个线程并发执行,并且不会因为加锁而阻塞线程,使得并发性能更好。
ConcurrentLinkedQueue 使用示例:
ConcurrentLinkedQueue concurrentLinkedQueue = new ConcurrentLinkedQueue();
concurrentLinkedQueue.add("Dog");
concurrentLinkedQueue.add("Cat");
while (!concurrentLinkedQueue.isEmpty()) {
System.out.println(concurrentLinkedQueue.poll());
}
执行结果:
Dog
Cat
可以看出不管是阻塞队列还是非阻塞队列,使用方法都是类似的,区别是底层的实现方式。
PriorityQueue 一个基于优先级堆的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator
进行排序,具体取决于所使用的构造方法。优先级队列不允许使用 null 元素。
PriorityQueue 代码使用示例 :
Queue priorityQueue = new PriorityQueue(new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
// 非自然排序,数字倒序
return o2 - o1;
}
});
priorityQueue.add(3);
priorityQueue.add(1);
priorityQueue.add(2);
while (!priorityQueue.isEmpty()) {
Integer i = priorityQueue.poll();
System.out.println(i);
}
程序执行的结果是:
3
2
1
PriorityQueue 注意的点 :
答:ArrayBlockingQueue 和 LinkedBlockingQueue 都实现自阻塞队列
BlockingQueue,它们的区别主要体现在以下几个方面:
答:add() 和 offer() 都是添加元素到队列尾部。offer 方法是基于 add 方法实现的,Offer 的源码如下:
public boolean offer(E e) {
return add(e);
}
答:Queue 属于一般队列,Deque 属于双端队列。一般队列是先进先出,也就是只有先进的才能先出;而双端队列则是两端都能插入和删除元素。
答:LinkedList 实现了 Deque 属于双端队列,因此拥有 addFirst(E)、addLast(E)、getFirst()、getLast()
等方法。
A:DelayQueue 内部是基于 PriorityQueue 实现的
B:PriorityBlockingQueue 不是先进先出的数据存储方式
C:LinkedBlockingQueue 默认容量是无限大的
D:ArrayBlockingQueue 内部的存储单元是数组,初始化时必须指定队列容量
答:C
题目解析:LinkedBlockingQueue 默认容量是 Integer.MAX_VALUE,并不是无限大的。
A:ArrayBlockingQueue 是线程安全的
B:ArrayBlockingQueue 元素允许为 null
C:ArrayBlockingQueue 主要应用场景是“生产者-消费者”模型
D:ArrayBlockingQueue 必须显示地设置容量
答:B
题目解析:ArrayBlockingQueue 不允许元素为 null,如果添加一个 null 元素,会抛 NullPointerException 异常。
PriorityQueue priorityQueue = new PriorityQueue();
priorityQueue.add(null);
System.out.println(priorityQueue.size());
答:程序执行报错,PriorityQueue 不能插入 null。
答:Java 中常见的阻塞队列如下:
答:有界队列和无界队列的区别如下。
答:说到延迟消息队列,我们应该可以第一时间想到要使用 DelayQueue
延迟队列来解决这个问题。实现思路,消息队列分为生产者和消费者,生产者用于增加消息,消费者用于获取并消费消息,我们只需要生产者把消息放入到
DelayQueue 队列并设置延迟时间,消费者循环使用 take() 阻塞获取消息即可。完整的实现代码如下:
public class CustomDelayQueue {
// 消息编号
static AtomicInteger MESSAGENO = new AtomicInteger(1);
public static void main(String[] args) throws InterruptedException {
DelayQueue delayQueue = new DelayQueue<>();
// 生产者1
producer(delayQueue, "生产者1");
// 生产者2
producer(delayQueue, "生产者2");
// 消费者
consumer(delayQueue);
}
//生产者
private static void producer(DelayQueue delayQueue, String name) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
// 产生 1~5 秒的随机数
long time = 1000L * (new Random().nextInt(5) + 1);
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 组合消息体
String message = String.format("%s,消息编号:%s 发送时间:%s 延迟:%s 秒",
name, MESSAGENO.getAndIncrement(), DateFormat.getDateTimeInstance().format(new Date()), time / 1000);
// 生产消息
delayQueue.put(new DelayedElement(message, time));
}
}
}).start();
}
//消费者
private static void consumer(DelayQueue delayQueue) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
DelayedElement element = null;
try {
// 消费消息
element = delayQueue.take();
System.out.println(element);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
// 延迟队列对象
static class DelayedElement implements Delayed {
// 过期时间(单位:毫秒)
long time = System.currentTimeMillis();
// 消息体
String message;
// 参数:delayTime 延迟时间(单位毫秒)
public DelayedElement(String message, long delayTime) {
this.time += delayTime;
this.message = message;
}
@Override
// 获取过期时间
public long getDelay(TimeUnit unit) {
return unit.convert(time - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
// 队列元素排序
public int compareTo(Delayed o) {
if (this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS))
return 1;
else if (this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS))
return -1;
else
return 0;
}
@Override
public String toString() {
// 打印消息
return message + " |执行时间:" + DateFormat.getDateTimeInstance().format(new Date());
}
}
}
以上程序支持多生产者,执行的结果如下:
生产者1,消息编号:1 发送时间:2019-6-12 20:38:37 延迟:2 秒 |执行时间:2019-6-12 20:38:39
生产者2,消息编号:2 发送时间:2019-6-12 20:38:37 延迟:2 秒 |执行时间:2019-6-12 20:38:39
生产者1,消息编号:3 发送时间:2019-6-12 20:38:41 延迟:4 秒 |执行时间:2019-6-12 20:38:45
生产者1,消息编号:5 发送时间:2019-6-12 20:38:43 延迟:2 秒 |执行时间:2019-6-12 20:38:45
…
队列(Queue)按照是否阻塞可分为:阻塞队列 BlockingQueue 和 非阻塞队列。其中,双端队列 Deque
也属于非阻塞队列,双端队列除了拥有队列的先进先出的方法之外,还拥有自己独有的方法,如
addFirst()、addLast()、getFirst()、getLast() 等,支持首未插入和删除元素。队列中比较常用的两个队列还有
PriorityQueue(优先级队列)和
DelayQueue(延迟队列),可使用延迟队列来实现延迟消息队列,这也是面试中比较常考的问题之一。需要面试朋友对延迟队列一定要做到心中有数,动手写一个消息队列也是非常有必要的。
IO 是 Input/Output 的缩写,它是基于流模型实现的,比如操作文件时使用输入流和输出流来写入和读取文件等。
传统的 IO,按照流类型我们可以分为:
其中,字符流包括 Reader、Writer;字节流包括 InputStream、OutputStream。
传统 IO 的类关系图,如下图所示:
了解了 IO
之间的关系,下面我们正式进入实战环节,分别来看字符流(Reader、Writer)和字节流(InputStream、OutputStream)的使用。
Writer 可用来写入文件,请参考以下代码:
// 给指定目录下的文件追加信息
Writer writer = new FileWriter("d:\\io.txt",true);
writer.append("老王");
writer.close();
这几行简单的代码就可以实现把信息 老王
追加到 d:\\io.txt
的文件下,参数二表示的是覆盖文字还是追加文字。
Reader 可用来读取文件,请参考以下代码:
Reader reader = new FileReader("d:\\io.txt");
BufferedReader bufferedReader = new BufferedReader(reader);
String str = null;
// 逐行读取信息
while (null != (str = bufferedReader.readLine())) {
System.out.println(str);
}
bufferedReader.close();
reader.close();
InputStream 可用来读取文件,请参考以下代码:
InputStream inputStream = new FileInputStream(new File("d:\\io.txt"));
byte[] bytes = new byte[inputStream.available()];
// 读取到 byte 数组
inputStream.read(bytes);
// 内容转换为字符串
String content = new String(bytes, "UTF-8");
inputStream.close();
OutputStream 可用来写入文件,请参考以下代码:
OutputStream outputStream = new FileOutputStream(new File("d:\\io.txt"),true);
outputStream.write("老王".getBytes());
outputStream.close();
上面讲的内容都是 java.io 包下的知识点,但随着 Java 的不断发展,在 Java 1.4 时新的 IO 包出现了
java.nio,NIO(Non-Blocking IO)的出现解决了传统 IO,也就是我们经常说的 BIO(Blocking IO)同步阻塞的问题,NIO
提供了 Channel、Selector 和 Buffer 等概念,可以实现多路复用和同步非阻塞 IO 操作,从而大大提升了 IO 操作的性能。
前面提到同步和阻塞的问题,那下面来看看同步和阻塞结合都有哪些含义。
组合方式 | 性能分析 |
---|---|
同步阻塞 | 最常用的一种用法,使用也是最简单的,但是 I/O 性能一般很差,CPU 大部分在空闲状态 |
同步非阻塞 | 提升 I/O 性能的常用手段,就是将 I/O 的阻塞改成非阻塞方式,尤其在网络 I/O |
是长连接,同时传输数据也不是很多的情况下,提升性能非常有效。 这种方式通常能提升 I/O 性能,但是会增加 CPU 消耗,要考虑增加的 I/O | |
性能能不能补偿 CPU 的消耗,也就是系统的瓶颈是在 I/O 还是在 CPU 上 | |
异步阻塞 | |
这种方式在分布式数据库中经常用到。例如,在往一个分布式数据库中写一条记录,通常会有一份是同步阻塞的记录,而还有两至三份是备份记录会写到其他机器上,这些备份记录通常都是采用异步阻塞的方式写 | |
I/O;异步阻塞对网络 I/O 能够提升效率,尤其像上面这种同时写多份相同数据的情况 | |
异步非阻塞 | 这种组合方式用起来比较复杂,只有在一些非常复杂的分布式情况下使用,像集群之间的消息同步机制一般用这种 I/O |
组合方式。例如,Cassandra 的 Gossip | |
通信机制就是采用异步非阻塞的方式。它适合同时要传多份相同的数据到集群中不同的机器,同时数据的传输量虽然不大,但是却非常频繁。这种网络 I/O | |
用这个方式性能能达到最高 |
了解了同步和阻塞的含义,下面来看 NIO 的具体使用 ,请参考以下代码:
int port = 6666;
new Thread(new Runnable() {
@Override
public void run() {
try (Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();) {
serverSocketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), port));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 阻塞等待就绪的 Channel
Set selectionKeys = selector.selectedKeys();
Iterator iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
try (SocketChannel channel = ((ServerSocketChannel) key.channel()).accept()) {
channel.write(Charset.defaultCharset().encode("老王,你好~"));
}
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// Socket 客户端 1(接收信息并打印)
try (Socket cSocket = new Socket(InetAddress.getLocalHost(), port)) {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(cSocket.getInputStream()));
bufferedReader.lines().forEach(s -> System.out.println("客户端 1 打印:" + s));
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// Socket 客户端 2(接收信息并打印)
try (Socket cSocket = new Socket(InetAddress.getLocalHost(), port)) {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(cSocket.getInputStream()));
bufferedReader.lines().forEach(s -> System.out.println("客户端 2 打印:" + s));
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
以上代码创建了两个 Socket 客户端,用于收取和打印服务器端的消息。
其中,服务器端通过 SelectionKey(选择键)获取到 SocketChannel(通道),而通道都注册到
Selector(选择器)上,所有的客户端都可以获得对应的通道,而不是所有客户端都排队堵塞等待一个服务器连接,这样就实现多路复用的效果了。多路指的是多个通道(SocketChannel),而复用指的是一个服务器端连接重复被不同的客户端使用。
AIO(Asynchronous IO)是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。
AIO 实现简单的 Socket 服务器,代码如下:
int port = 8888;
new Thread(new Runnable() {
@Override
public void run() {
AsynchronousChannelGroup group = null;
try {
group = AsynchronousChannelGroup.withThreadPool(Executors.newFixedThreadPool(4));
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(group).bind(new InetSocketAddress(InetAddress.getLocalHost(), port));
server.accept(null, new CompletionHandler() {
@Override
public void completed(AsynchronousSocketChannel result, AsynchronousServerSocketChannel attachment) {
server.accept(null, this); // 接收下一个请求
try {
Future f = result.write(Charset.defaultCharset().encode("Hi, 老王"));
f.get();
System.out.println("服务端发送时间:" + DateFormat.getDateTimeInstance().format(new Date()));
result.close();
} catch (InterruptedException | ExecutionException | IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) {
}
});
group.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// Socket 客户端
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
Future future = client.connect(new InetSocketAddress(InetAddress.getLocalHost(), port));
future.get();
ByteBuffer buffer = ByteBuffer.allocate(100);
client.read(buffer, null, new CompletionHandler() {
@Override
public void completed(Integer result, Void attachment) {
System.out.println("客户端打印:" + new String(buffer.array()));
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
Thread.sleep(10 * 1000);
A:createFile
B:exists
C:read
D:exist
答:B
A:同步操作不一定会阻塞
B:异步操作不一定会阻塞
C:阻塞一定是同步操作
D:同步或异步都可能会阻塞
答:C
题目解析:异步操作也可能会阻塞,比如分布式集群消息同步,采用的就是异步阻塞的方式。
答:它们三者的区别如下。
简单来说 BIO 就是传统 IO 包,产生的最早;NIO 是对 BIO 的改进提供了多路复用的同步非阻塞 IO,而 AIO 是 NIO
的升级,提供了异步非阻塞 IO。
答:使用 Java 7 提供的 Files 读取和写入文件是最简洁,请参考以下代码:
// 读取文件
byte[] bytes = Files.readAllBytes(Paths.get("d:\\io.txt"));
// 写入文件
Files.write(Paths.get("d:\\io.txt"), "追加内容".getBytes(), StandardOpenOption.APPEND);
读取和写入都是一行代码搞定,可以说很简洁了。
答:Files 是 Java 1.7 提供的,使得文件和文件夹的操作更加方便,它的常用方法有以下几个:
答:FileInputStream 可以实现文件的读取。
题目解析:因为 FileInputStream 和 FileOutputStream 很容易被记反,FileOutputStream
才是用来写入文件的,所以也经常被面试官问到。
A:InputStream
B:DataInputStream
C:BufferedReader
D:BufferedInputStream
E:OutputStream
F:BufferedOutputStream
答:D、F
题目解析:BufferedInputStream
是一种带缓存区的输入流,在读取字节数据时可以从底层流中一次性读取多个字节到缓存区,而不必每次都调用系统底层;同理,BufferedOutputStream
也是一种带缓冲区的输出流,通过缓冲区输出流,应用程序先把字节写入缓冲区,缓存区满后再调用操作系统底层,从而提高系统性能,而不必每次都去调用系统底层方法。
答:FileInputStream 在小文件读写时性能较好,而在大文件操作时使用 BufferedInputStream 更有优势。
Files.createFile(Paths.get("c:\\pf.txt"), PosixFilePermissions.asFileAttribute(
EnumSet.of(PosixFilePermission.OWNER_READ)));
A:在指定的盘符产生了对应的文件,文件只读
B:在指定的盘符产生了对应的文件,文件只写
C:在指定的盘符产生了对应的文件,文件可读写
D:程序报错
答:D
题目解析:本题目考察的是 Files.createFile 参数传递的问题,PosixFilePermissions 不支持 Windows,因此在
Windows 执行会报错 java.lang.UnsupportedOperationException: ‘posix:permissions’ not
supported as initial attribute。
在 Java 1.4 之前只有 BIO(Blocking IO)可供使用,也就是 java.io 包下的那些类,它的缺点是同步阻塞式运行的。随后在 Java
1.4 时,提供了 NIO(Non-Blocking IO)属于 BIO 的升级,提供了同步非阻塞的 IO 操作方式,它的重要组件是
Selector(选择器)、Channel(通道)、Buffer(高效数据容器)实现了多路复用的高效 IO 操作。而 AIO(Asynchronous
IO)也叫 NIO 2.0,属于 NIO 的补充和升级,提供了异步非阻塞的 IO 操作。
还有另一个重要的知识点,是 Java 7.0 时新增的 Files 类,极大地提升了文件操作的便利性,比如读、写文件
题目解析:异步操作也可能会阻塞,比如分布式集群消息同步,采用的就是异步阻塞的方式。
答:它们三者的区别如下。
简单来说 BIO 就是传统 IO 包,产生的最早;NIO 是对 BIO 的改进提供了多路复用的同步非阻塞 IO,而 AIO 是 NIO
的升级,提供了异步非阻塞 IO。
答:使用 Java 7 提供的 Files 读取和写入文件是最简洁,请参考以下代码:
// 读取文件
byte[] bytes = Files.readAllBytes(Paths.get("d:\\io.txt"));
// 写入文件
Files.write(Paths.get("d:\\io.txt"), "追加内容".getBytes(), StandardOpenOption.APPEND);
读取和写入都是一行代码搞定,可以说很简洁了。
答:Files 是 Java 1.7 提供的,使得文件和文件夹的操作更加方便,它的常用方法有以下几个:
答:FileInputStream 可以实现文件的读取。
题目解析:因为 FileInputStream 和 FileOutputStream 很容易被记反,FileOutputStream
才是用来写入文件的,所以也经常被面试官问到。
A:InputStream
B:DataInputStream
C:BufferedReader
D:BufferedInputStream
E:OutputStream
F:BufferedOutputStream
答:D、F
题目解析:BufferedInputStream
是一种带缓存区的输入流,在读取字节数据时可以从底层流中一次性读取多个字节到缓存区,而不必每次都调用系统底层;同理,BufferedOutputStream
也是一种带缓冲区的输出流,通过缓冲区输出流,应用程序先把字节写入缓冲区,缓存区满后再调用操作系统底层,从而提高系统性能,而不必每次都去调用系统底层方法。
答:FileInputStream 在小文件读写时性能较好,而在大文件操作时使用 BufferedInputStream 更有优势。
Files.createFile(Paths.get("c:\\pf.txt"), PosixFilePermissions.asFileAttribute(
EnumSet.of(PosixFilePermission.OWNER_READ)));
A:在指定的盘符产生了对应的文件,文件只读
B:在指定的盘符产生了对应的文件,文件只写
C:在指定的盘符产生了对应的文件,文件可读写
D:程序报错
答:D
题目解析:本题目考察的是 Files.createFile 参数传递的问题,PosixFilePermissions 不支持 Windows,因此在
Windows 执行会报错 java.lang.UnsupportedOperationException: ‘posix:permissions’ not
supported as initial attribute。
在 Java 1.4 之前只有 BIO(Blocking IO)可供使用,也就是 java.io 包下的那些类,它的缺点是同步阻塞式运行的。随后在 Java
1.4 时,提供了 NIO(Non-Blocking IO)属于 BIO 的升级,提供了同步非阻塞的 IO 操作方式,它的重要组件是
Selector(选择器)、Channel(通道)、Buffer(高效数据容器)实现了多路复用的高效 IO 操作。而 AIO(Asynchronous
IO)也叫 NIO 2.0,属于 NIO 的补充和升级,提供了异步非阻塞的 IO 操作。
还有另一个重要的知识点,是 Java 7.0 时新增的 Files 类,极大地提升了文件操作的便利性,比如读、写文件
Files.write()、Files.readAllBytes() 等,都是非常简便和实用的方法。