目录
一、Java面向对象
1.面向对象的特性及理解
2.访问修饰符都有哪些,它们的区别有哪些
3.为什么使用clone
4.浅拷贝和深拷贝
5.面向过程和面向对象的区别
6.成员变量和局部变量的区别
7.String 、StringBuilder 和 StringBuffer 的区别
8.Object类常用方法
二、JavaSE语法
1. & 和 && 的区别 ,那 | 和 ||
2.String类是否可以被继承
3.重载和重写的区别
4.抽象类和接口的区别
5.抽象方法可否被static、native和synchronized修饰
6.静态变量和实例变量的区别
7.equals和HashCode关系
8.两个对象的hashcode值相同,那equals是否一定为true
9.HashCode为什么使用31作为乘子
10.== 和 equals的区别
11.break 、 continue 和 return 的区别
12.String a = new String("abc"); 创建了几个对象
三、Java的异常处理
1.Java异常分哪些种类
2.finally执行是在return执行之前还是之后
3.几种最常见的运行期异常(RuntimeException)
4.throw 和 throws 的区别
5.final 、finally 和 finalize 的区别
6.finally不会被执行的情况
7.try-catch-finally
四、Java的数据类型
1.Java的基本数据类型及其占用字节,对应的包装类型
2.装箱和拆箱
五、Java的IO流
1.IO流分为几种
2.字节流和字符流的区别
3.nio
4.对象序列化和反序列化
六、Java的集合
1.集合总结
2.List,Set,Queue和Map的区别
3.集合框架的底层数据结构
4.ArrayList和Vector的区别
5.ArrayList和LinkedList的区别
6.ArrayList扩容机制
7.HashSet,LinkedHashSet和TreeSet的区别
8.Queue及其子类的特性
9.ArrayDeque和LinkedList的区别
10.HashMap
11.HashMap和Hashtable的区别
12.哈希冲突
Java的特性为以下几种:
继承:将不同类型对象所包含的相同属性、方法提取出来,构成一个父类,子类可以继承父类并获得它的所有属性和方法(private修饰的私有属性和方法除外)。子类对象可以使用父类的属性方法,也可以使用自己的,即对父类信息进行扩展。同时可以对父类的属性和方法进行修改并使用。
public class Main{
public static void main(String[] args) {
System.out.println("Hello World");
}
}
封装:将一个对象的信息(即属性)放到其对应类的内部,外部无法直接访问,只能够通过其提供的方法对隐藏的信息进行部分的操作和访问。一般只会将外界可以访问的属性通过方法来实现访问。
public class Person {
private Integer id;
private String name;
// 获取id方法
public Integer getId() {
return id;
}
// 设置id方法
public void setId(Integer id) {
this.id = id;
}
// 获取name方法
public String getName() {
return name;
}
// 设置name的方法
public void setName(String name) {
this.name = name;
}
}
多态:一个对象具有多种状态。其主要表现形式是在类的继承和接口实现的前提下,父类创建的对象由子类(继承类或实现类)去实现,即父类对象子类实现。其实例代码如下:
class Animal {
public double weight;
public double height;
public Animal(double weight, height) {
this.weight = weight;
this.height = height;
}
}
class Lion extends Animal {
super();
}
public class Main1 {
public static void main(String[] args) {
// 父类对象子类实现
Animal animal = new Lion();
}
}
如果子类重写了父类的方法。真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
class Person {
public int id;
public String name;
public Person() {
}
public Person(int id, String name){
this.id = id;
this.name = name;
}
public void say(){
System.out.println("person");
}
public void run(){
System.out.println("Person run");
}
}
class Child extends Person {
public Child() {
}
public void say() {
System.out.println("child");
}
/**
public void run(){
System.out.println("Child run");
}
**/
}
public class Main{
public static void main(String[] args){
Person person = new Child();
person.say();
person.run();
}
}
执行结果:
child
Person run
访问修饰符 | 当前类 | 当前包 | 子类 | 其他包 |
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default(默认,一般不写) | √ | √ | × | × |
private | √ | × | × | × |
Java中包含四种不同的修饰符,包括public,protected,default,private。
public:此修饰符的范围是所有类,都可以正常访问被修饰符修饰的部分。
protected:对于继承与此类的导出类或其他任何位于同一个包内的类来说都可以访问。
default:对于同一个包中的内容可见(不需要书写,默认就是被default修饰)。
private:被访问的范围只限于当前类中,其它均不可访问。
对应public,protected,default和private,它们的范围逐渐变小。
它们的适用范围如下:
外部类/接口 | 成员属性 | 方法 | 构造函数 | 成员内部类 | |
public | √ | √ | √ | √ | √ |
protected | √ | √ | √ | √ | |
default | √ | ||||
private | √ | √ | √ | √ |
在开发过程中,遇到情况。存在一个已经包含了有效值的对象A,这时需要一个与A完全相同的对象B,并且B的后续操作不会影响到A,即A和B是两个完全独立的对象,对象B由对象A来确定。此时可以用到clone()方法来快速高效地实现。
浅拷贝:对于基本数据类型采用值传递,对于引用数据类型采用引用传递,即拷贝后的对象仍然指向参考对象。如果这是拷贝对象修改了某些值,那么这些变动会影响到参考对象。
深拷贝:对于基本数据类型采用值传递,对于引用数据类型会创建一个新的对象,并将原有的内容复制给它。如果这是拷贝对象修改了某些值,那么这些变动不会影响到参考对象。
面向过程:面向过程的性能比面向对象高。因为类调用时需要实例化,开销大,比较消耗资源。但是面向过程没有面向对象易维护、易复用和易扩展。
面向对象:拥有对象封装、继承和多态的特性,实现系统的低耦合。但面向对象的性能要低于面向过程。
成员变量:在类中方法外创建,它属于类。可以被 public 、 private 、 static 等修饰符修饰。它存在于堆中,拥有默认值,可以不初始化调用。它随着对象的创建而创建,对象的消失而消失。
局部变量:在方法定义上或方法中创建,不能被修饰符和 static 修饰,但可以被 final 修饰。它存在于栈中,需要被初始化才能够使用。随着方法的调用而创建,方法调用完毕后消失。
String用来保存字符串,它被 final 关键字修饰,故不能够被继承,并且对象是不可变的。
StringBuilder 和 StringBuffer 提供了字符串拼接等方法,使得它们在操作字符串更加方便,并且在拼接大量字符串,它们的性能会更优秀。StringBuilder没有对方法加同步锁,属于非同步的,即非线程安全。而StringBuffer对方法加了同步锁,是线程安全的。
总结:
(1)String适用于操作少量数据;
(2)StringBuilder适用于单线程字符串缓冲区下操作大量数据;
(3)StringBuffer适用于多线程字符串缓冲区下操作大量数据。
(1)public final native Class getClass() 返回当前运行对象的Class对象。native修饰的方法由其他编程语言编写,一般为C或C++编写。final修饰,表明此方法不能够被重写。
(2)public native int hashCode() 返回调用对象的哈希码。
(3)public boolean equals(Object obj) 比较两个对象的内存地址是否相同,在使用时一般会将其重写,用来比较字符串的值是否相等。
(4)protected native Object clone() throws CloneNotSupportedException 常见并返回当前对象的拷贝,默认为浅拷贝。
(5)public String toString() 返回对象的字符串表现形式。
(6)public final native void notify() 随机唤醒一个处于等待的线程。
(7)public final native void notifyAll() 唤醒所有处于等待的线程。
(8)public final native void wait(long timeout) throws InterruptedException 使调用的线程进入等待,并且释放锁。参数timeout是等待时间。
(9)protected void finalize() throws Throwable 调用垃圾回收器将对象进行回收。
短路运算符:
&:逻辑与,它们的判别条件是,计算符号两边的值,如果有一个值为假,则返回假;如果两个值都为真,则返回真。
|:逻辑或,它们的判别条件是,计算符号两边的值,如果有一个值为真,则返回真;如果两个值都为假,则返回假。
断路运算符:
&&:逻辑与,它们的判别条件是,计算符号前边的值,如果值为假,则不计算后边的值,直接返回假;如果前后两个值都为真,则返回真。
||:逻辑或,它们的判别条件是,计算符号前边的值,如果值为真,则不计算后边的值,直接返回真;如果前后两个值都为假,则返回假。
String类由final关键字修饰,其不可以被继承。
重载:一般发生在同一个类中,方法名一致,参数列表的参数顺序、类型、个数不同。而方法返回值和修饰符可以被修改。
重写:发生在继承类或实现接口场景下,子类或实现类去实现父类或接口的方法,其返回值类型、方法名、参数列表必须相同。
区别点 | 重写方法 | 重载方法 |
发生范围 | 子类 | 同一个类 |
参数列表 | 一定不能修改 | 必须修改 |
返回类型 | 子类方法返回值应比父类方法返回值类型更小或相等 | 可修改 |
异常 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等 | 可修改 |
访问修饰符 | 一定不能做更严格的限制(可以降低限制) | 可修改 |
发生阶段 | 运行期 | 编译期 |
抽象类:
(1)可以定义构造方法,但不能创建实例对象;
(2)可以有抽象方法和具体方法(非抽象方法);
(3)抽象类的成员不都是public修饰的,成员可以被其他修饰符所修饰;
(4)抽象类中可以定义成员变量;
(5)包含抽象方法的类一定是抽象类,而抽象类不一定包含抽象方法;
(6)抽象类可以包含静态方法;
(7)一个类只能继承一个抽象类,并且实现(重写Override)其中的抽象方法,如果没有,那么这个类也必须是抽象类。
接口:
(1)接口不能定义构造方法;
(2)接口中只能定义抽象方法(没有方法体的方法),除了静态方法和默认方法(jdk8新特性);
(3)接口中的成员全部都是public修饰的;
(4)一个类可以实现多个接口,并重写其所有的抽象方法;
(5)可以定义成员变量,但必须赋予初始值,因为其成员变量是public static final类型的。
不能。
static修饰的方法不能够被重写,违反了抽象方法本来的用意。
native表示方法是由本地方法(C语言)实现,抽象方法是没有被重写的,即没有方法体。
synchronized用于处理方法的内部逻辑,而抽象方法没有内容。
静态变量被static修饰,它属于这个类而不属于实例,即每个实例拿到的值是相同的。变量在类被加载时被分配空间。
实例变量在类的实例被创建后才会分配空间,每个实例对象的变量是不共享的。
(1)在Object类中equals方法是直接调用的 “==” 去比较对象地址,然而其他类一般会去重写equals方法,像String类则是先比较地址,如果不相同则将其转换为字符数组进行逐一比较。如果对equals重写,则必须重新HashCode值。重写在于对同一对象的比较结果返回true,如果不修改HashCode就会导致equals相等而hashcode不等,产生程序Bug。
(2)hashcode相等,equals不一定相等;equals相等,hashcode一定相等。
(3)对于已经存入集合的同一对象,不能重写hashcode方法,否则可能导致内存泄漏
当需要将存入的数据删除时,首先会获取HashCode来确定其存在,此时的HashCode已不是当时的结果(假设已经修改了HashCode),所以也查不到相关值,于是便导致这个数据一直在内存中,无法释放,导致内存泄漏。
首先hashcode是将对象内容作为key计算,使用算法获取其hashcode值,取其整数结果时会出现不同对象hashcode值相同的情况,越糟糕的哈希算法越容易出现碰撞。
而equals的判定方法是比较两个对象的地址是否相同,也就是两个对象是否指向同一个地址。
所以,当对象HashCode值相等时,equals不一定相等;而当equals相等时,HashCode一定相等。
/**
* Returns a hash code for this string. The hash code for a
* {@code String} object is computed as
*
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
*
* using {@code int} arithmetic, where {@code s[i]} is the
* ith character of the string, {@code n} is the length of
* the string, and {@code ^} indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
主要原因分为如下两点:
(1)31作为奇质数的存在(若使用偶数则相当于移位运算,可能导致溢出,丢失数据),与33、37、39和41作为乘子测试后,哈希冲突均小于7。hashCode返回的是int类型,为了不丢失精度,Hash的最大值要小于2^31-1,而且31本身也是个适中的数字(不大不小),足够支持Hash的使用
(2)31 * i = (i << 5) - i
jvm会自动将乘法运算变成移运算和减法,性能更好
== :比较基本类型对象时,判断它们的数值是否相等;比较引用类型对象时,比较它们的地址是否相同。
equals :比较引用类型时,如果对equals方法进行重写,那么比较的是它们的值是否相等;如果没有进行重写,比较的是它们的地址。
注意:equals方法不能用于基本类型的对象比较!
break :用于强制退出循环,并结束这个循环,执行后面的语句。
continue :用于循环中,在满足条件时,跳过本次循环,继续执行后续的循环。
return :用于结束当前语句所在的方法,并返回相应的结果(如果 return 后面没有返回值,则直接结束方法执行)。
两个。字符串常量池中创建了一个 "abc" ,new String()在堆中开辟了一块空间,创建了一个对象。
Execption(异常)和Error(错误)拥有共同父类Throwable,它们都代表在程序中出现的错误,异常是可以被抛出的基本类型,能够被程序员使用代表进行处理。而Error用来表示编译时和系统错误,往往与程序员的操作无关。
Exception主要分为两种,一种是编译时异常,另一种则是运行时异常。
编译时异常指代码需要进行异常处理操作,否则无法通过编译,更不能运行。
运行时异常会自动被Java虚拟机抛出,所以不必在异常说明中把它们列出来。
如下代码中,try,catch,finally中都包含return语句。
首先try代码块中产生了异常,没有执行return语句,catch代码块继续执行,确定了返回值为11,但注意,这时并没有直接返回,而是继续执行finally,然后finally将返回值确定为12并直接返回(如果finally中有return语句,执行到此会直接返回)。
try {
int a = 10 / 0;
return 10;
} catch(Exception e) {
return 11;
} finally{
return 12;
}
总结:如果finally中没有return,finally的执行是在try和catch中的return执行之后,返回之前。如果finally中有return,finally会将try和catch中的return屏蔽(return也会执行,但仅仅是在之前确定返回值,并没有真正的返回),直接返回结果。
(1)java.lang.NullPointerException:空指针异常。由于调用了未经初始化或不存在的对象。
(2)java.lang.ClassNotFoundException:找不到指定类。由于类的名称和路径加载错误。
(3)java.lang.NumberFormatException:字符串转换异常。一般出现在字符串转数字类型时,字符串中包含非数字型类型。
(4)java.lang.IndexOutOfBoundsException:数组下标越界。出现访问了超出数组长度的下标操作。
(5)java.langIllegalArgumentException:方法传递参数异常。
(6)java.lang.ClassCastException:类型强制转换异常。
(7)java.lang.NoClassDefFoundException:未找到类定义错误。
(8)SQLException:SQL异常。常见于数据库操作时定义的SQL语句错误。
(9)java.lang.NoSuchMethodException:访问方法不存在。
throw 用于方法体内,用来抛出具体的异常,需要在内部对其进行异常处理。执行 throw 之后执行处理异常的操作或停止程序。
throws 用于方法上声明可能出现的异常,在后续的方法调用处抛出或处理异常。
final:用于修饰属性、方法和类。修饰属性初始化后不可变,修饰方法不能够被重写(可以重载),修饰类不能被继承。
finally:一般与try代码块搭配。finally中的代码一般情况下一定会被执行,且最后执行。
finalize:属于Object的子方法,在垃圾回收器执行的时候会调用被回收对象的此方法,用来回收对象资源。
(1)在finally代码块执行之前使用 System.exit() 强制结束Java虚拟机;
(2)finally 与 try 和 catch(可选)组合使用,如果try中的代码没有被执行,那么finally也不会被执行;
try:用于捕获异常,一般将可能出现异常的代码放入其中,后面接多个catch或一个finally。
catch:处理try中捕获到的异常。
finally:无论try和catch是否执行,finally中的代码一定会执行。一般会使用其关闭资源。
拓展:Java 1.7 新增try-with-resources 资源管理,原本在try中开启的资源可以创建在try()中,无需在 finally 块中关闭资源
// 使用自动资源管理开启资源
try (FileWriter fileWriter = new FileWriter("a/e.txt", true)){
String word = "测试文字";
fileWriter.write(word);
fileWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
Java共包含8种基本类型,分别为:
数字类型:byte、short、int、long、float、double
字符类型:char
布尔类型:boolean
8种数据类型的默认值、表示范围以及所占空间的大小如下:
基本类型 | 字节 | 默认值 | 取值范围 |
byte | 1 | 0 | -128~127 |
short | 2 | 0 | -32768~32767 |
int | 4 | 0 | -2147483648~2147483647 |
long | 8 | 0 | -9223372036854775808~9223372036854775807 |
float | 4 | 0.0 | 1.4E-45~3.4028235E38 |
double | 8 | 0.0 | 4.9E-324~1.7976931348623157E308 |
char | 2 | 0 | 一个字符,例如 'a', '你', '0' |
boolean | 1 | false | true, false |
基本类型所对应的包装器类型分别为:Byte、Short、Integer、Long、Float、Double、Character、Boolean。
注意:包装类已不属于基本类型,均属于引用类型,其初始值为 null 。
装箱:将基本类型转换为其对应的包装器类型。
拆箱:将包装器类型转换为所对应的基本类型。
// jdk 1.4
Integer a = new Integer(2); //装箱
int b = a.intValue(); //拆箱
// jdk 1.5 (从1.5开始,提供自动装箱拆箱)
Integer c = 3; //装箱,调用 valueOf()
int d = c; //拆箱,调用 intValue()
对于包装器类型,除了 Float 和 Double 没有常量池外,其他类型均具备常量池且有一定范围。Byte、Short、Integer、Long 的范围是[-128,127],而 Character 为[0,127]。
上述内容究竟在说什么呢,下面举例说明:
// Integer [-128,127]
Integer a = 127;
Integer b = 127;
Integer c = 128;
Integer d = 128;
// Character [0,127]
Character e = 127;
Character f = 127;
Character g = 128;
Character h = 128;
System.out.println(a == b); // true
System.out.println(c == d); // false
System.out.println(e == f); // true
System.out.println(g == h); // false
首先我们知道包装器类型是引用类型,所以 ”==“ 比较的是指向堆中地址是否相同。
观察上述结果,可以发现在范围内比较时,相同值相等,即指向同一地址。这是因为在范围内时,直接使用的是常量池中的对象。
而超出范围情况下,值虽然相同,但结果却不相等。此时等价于 Integer c = new Integer(128); 会直接创建对象。
所以,包装类之间比较时,使用 equals 方法来验证值是否相等。
Java IO 流的40多个类都是如下4个抽象类基类中派生出来的。
按照操作方式分类结构图:
按照操作对象分类结构图:
字节流:以字节为单位处理文件,支持所有类型文件的I/O处理,例如:图片,视频,音乐等。但在处理文本数据时,会先将文件以字节方式读取,然后再查询编码表将字节转换为字符返回,导致其效率不高,故不建议使用字节流处理纯文本文件。
字符流:以2个字节的Unicode字符为单元数据处理文件,代表:Reader和Writer。为了便于处理16位的Unicode字符而设计,让所有的I/O操作都支持Unicode,对于文本数据要优先考虑字符流。
注意:字节流可处理任何类型的文件,但字符流只能处理纯文本文件
Java nio 也叫 New IO,在 Java1.4 中引入的 java.nio.*包,其中使用了新的I/O类库,其本质目的是要提高处理速度。与传统I/O的区别是新增了通道和缓冲器,在处理数据时,直接与通道(数据)交互的是缓冲器,我们向缓冲器发送文件或获取文件。
Java对象保存到数据文件中,将实现Serializable接口的对象转换成一个字节序列,并能够恢复成原来的Java对象,即对象的序列化和反序列化。
利用序列化可以实现轻量级持久性,意味着一个对象的生存周期不取决于程序是否正在运行,它可以生存在程序之间的调用。通过将一个序列化对象写入磁盘,然后再重新执行程序时恢复该对象,就能够实现持久化的效果。
(1)创建Person对象,实现Serializable接口
class Person implements Serializable {
public int id;
public String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
(2)实现对象序列化
// 创建Person实例对象
Person person = new Person(1, "Li Honghao");
// 将Person对象进行序列化
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream("a/person.txt")))) {
// 调用writeObject()将对象序列化到文件中
objectOutputStream.writeObject(person);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
(3)实现对象反序列化
// 从文件中读取Person对象恢复
try (ObjectInputStream objectInputStream = new ObjectInputStream(new BufferedInputStream(new FileInputStream("a/person.txt")))){
// 调用readObject()获取文件中的对象
Object o = objectInputStream.readObject();
// 判断实例对象是否属于Person
if (o instanceof Person) {
Person p = (Person) o;
System.out.println(p);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Java集合也叫Java容器,Java使用类库提供了一套相当完整的容器类用来保存对象,其中基本的类型是List、Set、Queue和Map,这些对象类型也称为集合类。
Collection:
实现自Iterable接口,包括List,Queue和Set类型,这些元素都服从一条或多条规则。
List按照插入的顺序保存元素,Set不能存重复元素,Queue按照先进先出原则。
Map:
以键值对(key-Value)形式存储元素,使用键来查询值。其他类实现Map接口,其中出现次数较多的为HashMap。
List:存储元素的特点为有序可重复的。
Set:存储元素的特点为无须且不可重复的。
Queue:存储元素的特点为有序可重复的,其按照先进先出,后进后出的原则,可以想像是排队的场景。
Map:使用键值对存储,其key是无序的不能重复的,value是无序可重复的
tips:Map在其他资料中对Key和Value能不能为空的问题是绝对的,但实际需要对其实现子类具体分析,例如HashMap的Key、Value可以为null,而HashTable的Key和Value均不能为null。
List
ArrayList:Object[] 数组
Vector:Object[] 数组
LinkedList:双向链表,由一个个Node组成,每个Node包含数据,前驱节点信息和后继节点信息
Set
HashSet:底层基于HashMap实现,因此其结构为数组+链表(jdk1.8 添加红黑树)
LinkedHashSet:底层是LinkedHashMap,属于数组+双向链表结构
TreeSet:红黑树结构,是一颗平衡的排序二叉树
Queue
PriorityQueue:Object[] 数组,是一个平衡的二叉堆
ArrayQueue:Object[] 数组,拥有头尾的双向循环数组
Map
HashMap: JDK1.8之前是数组+链表结构,数组的每一个项对应着一个链表。JDK1.8后,增加红黑树结构提升效率,当链表长度大于8,且数组长度等于64时转换为红黑树,如果数组长度不足64时,会先进行数组扩容而不变为红黑树
LinkedHashMap:继承自HashMap,其结构相似,同样是数组+链表+红黑树。单独使用了双向链表用于保存其数据顺序。当迭代遍历它时,取得“键值对”的顺序是其插入顺序,或依照最近最少使用原则的次序
HashTable:其结构为数组+链表,采用哈希进行数据插入分组
TreeMap:基于红黑树实现。查看 “键” 或 “键值对” 时,其结果是经过排序的。
(1)效率不同,因为Vector是线程安全的,而ArrayList不是。所以Vector的效率不如ArrayList
(2)扩容大小不同,ArrayList和Vector均属于动态可变长数组,添加元素空间不足时,Vector为扩容至原长的2倍,而ArrayList会扩容到1.5倍
(1)两者底层结构不同,ArrayList结构为数组,而LinkedList结构是双向链表(每个节点包含前驱指针、后继指针和数据,内存存储不一定连续)
(2)ArrayList用于查找多,删除和添加元素较少的场景;LinkedList擅长删除和添加元素,查找元素较慢。
ArrayList插入和删除元素时间复杂度受元素位置影响。当调用add()方法不指定元素位置时,会插到队列的末尾,时间复杂度为O(1);若要修改第i位的元素时,第i位和第i+1位的元素会向后移动(删除元素类似),时间复杂度为O(n-i)。而LinkedList采用链表式结构,对于头部和尾部的元素操作没有影响,时间复杂度为O(1);在第i位操作时,不涉及前后节点的变动,只涉及第i位前后节点的前驱后继信息的更新,但需要先去找到第i位元素的位置,由于不是顺序存储,故时间复杂度为O(n)。
(3)ArrayList支持快速随机访问,而LinkedList不支持。
快速随机访问取决于其底层结构,ArrayList的底层结构为数组,是一块连续的内存空间,每一个元素通过下标进行标识,因此随机访问元素的时间复杂度为O(1)。而LinkedList的底层结构为链表,是一个不连续的存储空间,当进行元素访问时,需要去遍历链表找到对应的元素,其时间复杂度为O(n)。对于支持快速随机访问的类,通过实现RandomAccess接口来进行标识。
注意:RandomAccess接口中没有定义任何属性和方法,只是作为具备快速随机访问能力的标识。
(4)ArrayList 占用内存空间比LinkedList少。
LinkedList的每个节点不仅储存这数据,还保存着它的前驱和后继节点的地址。ArrayList空间占用体现在尾部的余量空间,可以通过设置集合长度和合理分配资源来优化。
ArrayList 初始默认大小为10,也可在创建对象时指定集合容量。
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
}
}
每次调用add方法时首先判断是否需要扩容,扩容的大小为原集合的1.5倍(int newCapacity = oldCapacity + (oldCapacity >> 1);)
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return true (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
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);
}
区别点/集合 | HashSet | LinkedHashSet | TreeSet |
---|---|---|---|
底层实现结构 | 实现自HashMap,结构为数组+链表(+红黑树 jdk1.8) | HashMap + 双向链表维护顺序 | 红黑树,一颗自平衡二叉查找树 |
线程安全 | 不支持 | 不支持 | 不支持 |
使用场景 | 不维护顺序,保证元素唯一性 | 维护元素顺序,支持插入和访问顺序,默认插入,满足FIFO(先进先出)场景 | 元素有序,支持自然排序和定制排序,内部提供Comparator进行元素比较,例如实现关联数组 |
时间复杂度 | 插入、删除和查找元素时间复杂度为O(1) | 插入、删除和查找元素时间复杂度为O(1) | 插入、删除和查找元素时间复杂度为O(log(n)) |
性能 | 比LinkedHashSet、TreeSet更好,是三者中最快的,原因是不维护元素顺序 | 与HashSet基本相同,但稍慢一点,要优于TreeSet | TreeSet最慢,因为在做插入和删除后需要维护元素的顺序 |
元素去重 | 通过HashCode和equals进行判断,如果HashCode不等,则插入元素;HashCode相等判断equals是否相等,不等插入元素,相等则跳过此元素 | 与HashSet相同,使用HashCode和equals判断 | 使用compare和compareTo比较对象 |
允许空值 | 只允许存在一个空值。反复插入空值不会报错,而是在集合中留下一个空值存在 | 只允许有一个空值 | 不允许插入空值,插入会报NullPointerException |
Queue
Queue是一个满足先进先出(FIFO)的结构,支持在队尾添加元素和队头获取删除元素。而对于每种操作,Queue定义了两种处理失败返回方式不同的方法,下面的表格列出。
操作行为/失败结果反馈 | 抛出异常 | 其他类型值返回 |
---|---|---|
插入元素 | add() | offer() |
取出队首元素(查看元素并删除) | remove() | poll() |
查看队首元素(查看但不删除) | element() | peek() |
对于上述的几种方法,根据需求进行选择。注意不要添加 null 元素,对于 poll() 和 peek() 方法,如果是一个空的队列,方法本身会返回 null ,这会导致返回的结果产生混淆。
PriorityQueue
PriorityQueue是一个操作支持优先级的队列,元素不会按照插入顺序被取出,而是通过 Comparator 决定元素出队的优先级,因此 PriorityQueue 所存的对象必须实现 Comparator 接口或者在创建 PriorityQueue 对象时传入自定义的 Comparator 对象作为参数。
若不指定比较器,则采用自然排序
另外 PriorityQueue 不支持线程安全,可以使用 PriorityBlockingQueue 处理多线程情况。
// 自定义Comparator对象
public class PriorityQueueTest {
public static void main(String[] args) {
Queue queue = new PriorityQueue<>(new MyComparator());
queue.add(new User(1, "XiaoLi", "pass123"));
queue.add(new User(2, "XiaoHua", "pass456"));
queue.add(new User(3, "John", "pass789"));
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
}
}
/**
* 自定义比较器
*/
class MyComparator implements Comparator {
/**
* 计算User对象name属性的长度
* @param o1 the first object to be compared.
* @param o2 the second object to be compared.
* @return 1 0 -1
*/
@Override
public int compare(User o1, User o2) {
String u1 = o1.getName();
String u2 = o2.getName();
if (u1.length() > u2.length()) {
return 1;
} else if (u1.length() == u2.length()) {
return 0;
}
return -1;
}
}
class User {
private int id;
private String name;
private String password;
public User(int id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", password='" + password + '\'' +
'}';
}
}
Deque
Deque本质是一个双端队列,即允许队头队尾都可以执行插入和取出操作,因此提供的方法会区分具体是操作队头还是队尾,下面还是以返回类型作为区分来展示。
操作行为/失败结果反馈 | 抛出异常 | 其他类型值返回 |
---|---|---|
插入元素-队首 | addFirst() | offerFirst() |
插入元素-队首 | addLast() | offerLast() |
取出队首元素(查看元素并删除)-队首 | removeFirst() | pollFirst() |
取出队首元素(查看元素并删除)-队尾 | removeLast() | pollLast() |
查看队首元素(查看但不删除)-队首 | getFirst() | peekFirst() |
查看队首元素(查看但不删除)-队尾 | getLast() | peekLast() |
可以看到与 Queue 提供的方法基本相同,只是添加了 First 和 Last 后缀进行区分。从源码上看,Deque 继承了 Queue,因此 Queue 的方法 Deque 也能使用,但不建议,对于双端队列还是用使用指定队首队尾的方法。
另外,Deque 提供了 push 和 pop 方法用于模拟栈。
注意:Deque的仅查看元素但不删除的方法是 getFirst 和 getLast,与 Queue 的 element 方法名不同,注意区分。
ArrayDeque 和 LinkedList 都实现了Deque接口,那两者的区别是什么呢?
ArrayDeque | LinkedList | |
---|---|---|
线程安全 | 不支持 | 不支持 |
底层实现 | 可变长数组和双指针,初始化一个16长的数组 | 双向链表 |
允许空值 | 不允许 | 允许 |
扩容机制 | 当双端队列已满是,调用扩容机制,大小为原来的2倍(n<<1) | 没有所谓的扩容机制,因为其本身就是没有长度限制的,每次添加元素时申请新堆空间 |
时间复杂度 | O(1) 因为其本身是双端队列,插入和删除只能在两端完成 | O(n) 内存地址不连续,每次查询只能通过头或尾部进行依次查找 |
一个以 key-value 键值对形式存储元素的集合,对于它的底层结构在jdk1.8之前采用的数据+链表的形式,而jdk1.8之后新增红黑树结构(当元素过多时提升查询效率)。
扩容机制
当数组中元素的个数大于阀值(数组长度 * 加载因子)时进行重新哈希,创建一个基于原数组长度2倍的数组并重新哈希分配元素位置,也叫扩容
转红黑树的条件
当链表长度大于等于8,且数组长度大于64时进行链表转红黑树,如果小于64则先进行扩容不转换结构。当红黑树节点个数小于等于6时,退回链表。
遍历集合的方法
Map map = new HashMap<>(20);
/..插入数据../
//1.通过自带方法获取结果集
// keySet()获取key集合
for(String key : map.keySet()) {
System.out.println(key);
}
// values()获取value集合
for (String value : map.values()) {
System.out.println(value);
}
// 2.keySet()获取key集合,再通过get方法获取value
for (String key : map.keySet()) {
String value = map.get(key);
System.out.println("key:" + key + " value:" + value);
}
// 3.通过entrySet()获取
for (Map.Entry entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
System.out.println("key:" + key + " value:" + value);
}
// 4.使用Iterator迭代器
Iterator> iterator = map.entrySet().iterator();
if (iterator.hasNext()) {
Map.Entry entry = iterator.next();
String key = entry.getKey();
String value = entry.getValue();
System.out.println("key:" + key + " value:" + value);
}
// 5.使用Lambda表达式
map.forEach((key, value) -> {
System.out.println("key:" + key + " value:" + value);
});
HashMap | Hashtable | |
---|---|---|
继承类 | AbstractMap | Dictionary |
底层结构(jdk1.8) | 数组+链表+红黑树 | 数据+链表 |
线程安全 | 不支持,多线程下会出现死循环(可替代线程安全集合-ConcurrentHashMap) | 支持,大部分方法均通过synchronized关键字修饰 |
效率 | 高于Hashtable | 由于保证线程安全的缘故,效率低于HashMap |
允许空值 | 支持一个null键和多个null值 | 不支持null键和值 |
默认构造容量 | 16 | 11 |
哈希算法是对任意一组数据进行计算,得出固定长度的返回结果。其特点就是相同的输入一定得到相同的输出,不同的输入大概率得到不同的输出。部分集合采用对元素key进行hash来决定在表中的位置,当有一堆数据做插入操作时,很可能相同的位置已经存在元素,因为他们的hash结果是相同的。导致多个数据要放到一个位置上,产生了数据的冲突。基于上述问题,有下面的几种方法可以解决。
开放定址法
当冲突发生时,根据一定方式(再次散列)查找下一个空闲单元或不冲突的位置放入。查询时通过Hash计算位置,当与对应位置元素比较相等,则找到结果;若不等,则说明在插入时发生了哈希冲突,则按照使用的散列方式进行查找位置比较元素,直到找到元素。
特点:容易产生数据堆积,不适用于大规模数据存储。插入时会出现多次冲突的现象,删除的元素十多个冲突元素的其中之一,需要后面对元素作处理,实现比较复杂。同时对于元素的删除,节点不能简单的将空间置为空,否则会导致后续的填入散列表的同义词节点的查找路径将被截断,查询元素时错误的返回失败,导致结果不正确。所以在删除元素时,只能做一个删除的标记,而不能真正的去删除。
线性探测
当冲突发生时,按顺序查找到下一个空闲单元,如果到结尾还没有找到位置,则从头开始继续寻找。
$$
i,i+1,i+2,i+3,i+4........
$$
二次探测
在当前冲突的左右依次跳跃寻找为空的位置。
$$
i,i+1,i-1,i+2,i-2.........
$$
伪随机探测
通过随机数发生器生成一个随机数组,每次探寻时通过随机数作为间隔进行查找。
int[] random = {1, 3, 12, 13, 7, 2}
$$
i+1,i+3,i+12,i+13,i+7, i+2
$$
平方探测法
冲突单元添加正数的平方进行空闲单元的查找。
$$
i + 1^2, i + 2^2, i + 3^2, i + 4^2 .....
$$
再哈希法
准备多个不同的哈希函数,当出现哈希冲突时,调用其他哈希函数进行重新计算,直到不冲突为止。
$$
Hi=RH1(key) i=1,2,…,k
$$
当计算 RH1(key) 出现冲突时,计算 RH2(key) ........
特点:不易产生数据聚集,但增加了计算时间。
链地址法
将哈希地址相同的元素构成一个链表,并将链表的头指针放入对应单元中,每次新添加一个相同哈希地址的元素,新建一个节点添加进去。适用于频繁插入和删除。
特点:处理方法加单,不产生数据堆积,平均查找长度较短。链表上节点根据数据变动产生动态变化,适合创建前无法确定长度的情况。删除元素时依照链表的结构看,操作比较简单,删除节点即可。
建立公共溢出区
为所有冲突的关键字建立一个公共的存放区,当在基本表出现哈希冲突时,将其填入溢出表。查找元素时,先进行哈希比较基本表元素,如果查到则返回;若基本表不存在值,则在溢出表中进行顺序查找。