Q:静态方法为什么不能调用非静态成员?
A:
静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化后才存在,需要通过类的实例对象去访问。
在类的非静态成员不存在的时候,静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
Q:静态方法和实例方法的区别
A:
(首先明白类和对象的关系。对象是类new出来的,类是创建对象的。对象是类的实例,所以实例方法需要实例(对象)来调用)
调用方式:静态方法属于“类”本身,所以调用静态方法可以直接使用
类名.方法名
,也可以使用对象.方法名
(不推荐),而调用实例方法就要使用对象.方法名
来调用-
访问类成员是否有限制:
静态方法在访问本类的成员时,只允许访问静态成员(静态成员变量和静态方法),不允许访问实例成员,而实例方法不存在这个限制
Tips:重写:如果父类方法访问修饰符为private/final/static,则子类不能重写该方法。
Tips:可变长参数:
从Java5开始,Java支持定义可变长参数,即允许在调用方法时传入不定长度的参数。另外,可变长参数只能作为函数的最后一个参数,但是前面可以有也可以没有任何其他参数。
public static void method1(String... args){
//.......
}
方法重载的时候会优先匹配固定参数的方法,因为固定参数的方法匹配度更高
Tips:Java中的8种基本数据类型:
-
6种数字类型:
4种整数型:
byte
、short
、int
、long
2种浮点型:
float
、double
1种字符类型:
char
1种布尔类型:
boolean
注意:
Java里使用long类型的数据要在数值后面加上L,否则将作为整型解析
char a='h'
char:单引号,string b="hello"
string:双引号。这8种基本数据类型都有对应的包装类:
Byte
、Short
、Integer
、Float
、Long
、Double
、Charachter
、Boolean
Q:基本类型和包装类型的区别?
A:
包装类型不赋值就是
null
,基本类型不赋值有默认值且不是null
包装类型可以用于泛型,基本类型不可以🙅♂️
基本数据类型的局部变量存放在Java虚拟机栈中的局部变量表中,基本数据的成员变量(未被
static
修饰)存放在Java虚拟机的堆中。包装类型属于对象类型,几乎所有对象实例都存在于堆中相比于对象类型,基本数据类型占用的空间非常小
Q:什么是自动拆、装箱?
A:
装箱:将基本类型用它们对应的引用类型包装起来(调用包装类的
valueOf()方法
)拆箱:将包装类型转换为基本数据类型(调用
xxxValue()方法
)
eg:
Integer i = 10; //装箱
int n = i; //拆箱
注意:如果频繁装拆箱的话,也会严重影响系统的性能。所以尽量避免不必要的拆装箱操作。
Q:面向对象和面向过程的区别
A:两者主要区别在于解决问题的方式不同:
面向对象会先抽象出对象,然后用对象执行方法的方式解决问题
面向过程把解决问题的过程拆分成一个个方法,通过一个个方法的执行解决问题
Tips:面向对象开发的程序一般更易维护、易复用、易扩展
Q:成员变量和局部变量的区别
A:
-
语法形式上:成员变量是属于类的,而局部变量在代码块中或方法中定义的变量或是方法的参数;
成员变量可以被
public/private/static
等修饰符修饰,而局部变量不能被访问控制修饰符及static
修饰;但是,成员变量和局部变量都可以被
final
所修饰。 -
存储方式上:如果成员变量是使用
static
修饰的,那么这个成员变量是属于类的,否则属于实例对象的。而对象存在于堆内存,局部变量则存在于栈内存。
生存时间上:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用自动生成,随着方法的结束调用而消亡。
默认值上:成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(例外:被
final
修饰的成员变量必须显式地赋值),而局部变量则不会自动赋值。
Q:类的构造方法的作用是什么?
A:主要是完成对象的初始化工作。
Tips:构造方法特点:
名字与类名相同
没有返回值,但不能用void声明构造函数
生成类的对象时自动执行,无需调用
构造方法不能被重写(override),但是可以重载
面向对象三大特征
- 封装
封装是指把一个对象的状态信息(即属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
public class Man {
private int id; //将id属性私有化
private String name; //将name属性私有化
//提供访问属性的方法
public int getId(){
return id;
}
}
-
继承
子类可以增加新的数据或功能,也可以用父类的功能,但不能选择性地继承父类(通过使用继承,可以快速地创建新的类,可以提高代码的重用性,程序的可维护性,节省大量创建新类的时间,提高开发效率)
子类拥有父类对象所有属性和方法(包括私有属性和私有方法),但是父类中的私有属性和私有方法子类时无法访问的,只是拥有。
子类可以拥有自己的属性和方法。
子类可以用自己的方式实现父类的方法。
-
多态
表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例
对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
多态不能调用“只在子类存在但在父类不存在的方法”;
如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果没有覆盖父类方法,执行的才是父类的方法。
Q:接口和抽象类的共同点和差别?
A:共同点:
都不能被实例化
都可以包含抽象方法
-
都可以有默认实现的方法(Java8可以用
default
关键字在接口中定义默认方法)区别:
接口主要用于对类的行为进行约束,实现了某个接口就具有了对应的行为。抽象类主要用于代码的复用,强调的是所属关系
-
一个类只能继承一个类,但可以实现多个接口
- 接口中的成员变量只能是
public static final
类型的,不能被修改且必须有初始值,而抽象类的成员变量默认default
,可在子类中重新被定义,也可以重新被赋值
- 接口中的成员变量只能是
Tips:
浅析深拷贝和浅拷贝:
浅拷贝:拷贝对象和原对象共用一个内部对象(复制了一个外皮,共用内在?)
深拷贝:拷贝对象时把内部对象也拷贝了(内在也一起复制),拷贝对象和原对象用的就是不同的对象了。
Q:==和equals()的区别?
对于基本数据类型来说,
==
比较的是值对于引用数据类型来说,
==
比较的是对象的内存地址
String
中的equals
方法是被重写过的,Object
的equals
方法是比较对象的内存地址,而String
的equals
方法比较的是对象的值。
当创建String
类型的对象时,虚拟机会在常量池中查找有没有值相同的已经存在的对象,有就赋给当前引用,没有就在常量池中重新创建一个String
对象。
Tips:hashCode
的作用是获取哈希码,也称散列码,作用是确定该对象在哈希表中的索引位置。该方法通常用来将对象的内存地址转化为整数之后返回。
hashCode()
和equals()
都用于比较两个对象是否相等
如果两个对象的
hashCode
值相等,这两个对象不一定相等(哈希碰撞)如果两个对象的
hashCode
值相等并且equals()
返回true,认为这两个对象相等如果两个对象的
hashCode
不相等,可以直接认为两个对象不相等。
Q:String、StringBuffer、StringBuilder的区别?
- 可变性
String
类中使用 final
关键字修饰字符数组来保存字符串,所以String
对象是不可变的。
final
关键字修饰的数组保存字符串并不是String
不可变的根本原因,因为这个数组保存的字符串是可变的(final
修饰引用类型变量的情况)。
String
真正不可变有下面几点原因:
- 保存字符串的数组被
final
修饰且为私有的,并且String
类没有提供/暴露修改这个字符串的方法。
String
类被final
修饰导致其不能被继承,进而避免了子类破坏String
不可变。
StringBuilder
与 StringBuffer
都继承自 AbstractStringBuilder
类,在 AbstractStringBuilder
中也是使用字符数组保存字符串,不过没有使用 final
和 private
关键字修饰,最关键的是这个 AbstractStringBuilder
类还提供了很多修改字符串的方法比如 append
方法。
- 线程安全性
String
中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder
是 StringBuilder
与 StringBuffer
的公共父类,定义了一些字符串的基本操作。StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
- 性能
每次对 String
类型进行改变的时候,都会生成一个新的 String
对象,然后将指针指向新的 String
对象。StringBuffer
每次都会对 StringBuffer
对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder
相比使用 StringBuffer
仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
Tips:
对于三者使用的总结:
操作少量的数据: 适用
String
单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
字符串拼接问题
String str4 = str1 + str2 + str3; 字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 for (int i = 0; i < arr.length; i++) { s += arr[i]; } 在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象,意味着每循环一次就会创建一个 StringBuilder 对象。 如果直接使用 StringBuilder 对象进行字符串拼接的话,就不会存在这个问题了 String[] arr = {"he", "llo", "world"}; StringBuilder s = new StringBuilder(); for (String value : arr) { s.append(value); } System.out.println(s);
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder
调用 append()
方法实现的,拼接完成之后调用 toString()
得到一个 String
对象 。
eg: String str4 = new StringBuilder().append(str1).append(str2).toString();
写代码的时候,尽量避免多个字符串对象拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder
或者 StringBuffer
。
异常
RuntimeException
及其子类都统称为非受检查异常,受检查异常没有被 catch
或者throws
关键字处理的话,就没办法通过编译。
NullPointerException
(空指针错误)IllegalArgumentException
(参数错误比如方法入参类型错误)NumberFormatException
(字符串转换为数字格式错误,IllegalArgumentException
的子类)ArrayIndexOutOfBoundsException
(数组越界错误)ClassCastException
(类型转换错误)ArithmeticException
(算术错误)SecurityException
(安全错误比如权限不够)UnsupportedOperationException
(不支持的操作错误比如重复创建同一用户)
Throwable 类常用方法有哪些?
String getMessage()
: 返回异常发生时的简要描述String toString()
: 返回异常发生时的详细信息String getLocalizedMessage()
: 返回异常对象的本地化信息。使用Throwable
的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()
返回的结果相同void printStackTrace()
: 在控制台上打印Throwable
对象封装的异常信息
try-catch-finally 如何使用?
try
块 : 用于捕获异常。其后可接零个或多个catch
块,如果没有catch
块,则必须跟一个finally
块。catch
块 : 用于处理 try 捕获到的异常。finally
块 : 无论是否捕获或处理异常,finally
块里的语句都会被执行。当在try
块或catch
块中遇到return
语句时,finally
语句块将在方法返回之前被执行。
注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。
在某些情况下,finally 中的代码不会被执行。
-
虚拟机被终止运行的话,finally 中的代码就不会被执行。
System.exit(1);
程序所在的线程死亡。
关闭 CPU。
什么是泛型?有什么作用
Java 泛型(Generics)是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
反射
通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
反射的优缺点:
优点 : 可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利
缺点 :让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。
注解
注解只有被解析之后才会生效,常见的解析方法有两种:
编译期直接扫描 :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用
@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。运行期通过反射处理 :像框架中自带的注解(比如 Spring 框架的
@Value
、@Component
)都是通过反射来进行处理的。
I/O
什么是序列化?什么是反序列化?
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
简单来说:
序列化: 将数据结构或对象转换成二进制字节流的过程
反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。
序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
Java 序列化中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,使用 transient
关键字修饰。
transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient
修饰的变量值不会被持久化和恢复。
关于 transient
还有几点注意:
transient
只能修饰变量,不能修饰类和方法。transient
修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰int
类型,那么反序列后结果就是0
。static
变量因为不属于任何对象(Object),所以无论有没有transient
关键字修饰,均不会被序列化。
获取用键盘输入常用的两种方法
方法 1:通过 Scanner
Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close();
方法 2:通过 BufferedReader
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();
Java 中 IO 流分为几种?
按照流的流向分,可以分为输入流和输出流;
按照操作单元划分,可以划分为字节流和字符流;
按照流的角色划分为节点流和处理流
InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
按操作对象分类结构图:
[图片上传失败...(image-52a426-1655452481351)]
Q:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
A:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
实参&形参
实参(实际参数) :用于传递给函数/方法的参数,必须有确定的值。
形参(形式参数) :用于定义函数/方法,接收实参,不需要有确定的值。
值传递&引用传递
程序设计语言将实参传递给方法(或函数)的方式分为两种:
值传递 :方法接收的是实参值的拷贝,会创建副本。
引用传递 :方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。
Java只有值传递!!!
代理模式
代理模式简单来说就是 我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。
代理模式有<u style="box-sizing: border-box;">静态代理</u>和<u style="box-sizing: border-box;">动态代理</u>两种实现方式
静态代理
静态代理中,我们对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。
静态代理实现步骤:
定义一个接口及其实现类;
创建一个代理类同样实现这个接口
将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
动态代理
相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类。
从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
说到动态代理,Spring AOP、RPC 框架应该是两个不得不提的,它们的实现都依赖了动态代理。
集合
Java 集合, 也叫作容器,主要是由两大接口派生而来:一个是 Collection
接口,主要用于存放单一元素;另一个是 Map
接口,主要用于存放键值对。对于Collection
接口,有三个主要的子接口:List
、Set
和 Queue
。
Q:List, Set, Queue, Map 四者的区别?
List
(对付顺序的好帮手): 存储的元素是有序的、可重复的。Set
(注重独一无二的性质): 存储的元素是无序的、不可重复的。Queue
(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。Map
(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),"x" 代表 key,"y" 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
List
Arraylist
:Object[]
数组Vector
:Object[]
数组LinkedList
: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
如何选用集合
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用 Map
接口下的集合,需要排序时选择 TreeMap
,不需要排序时就选择 HashMap
,需要保证线程安全就选用 ConcurrentHashMap
。
当我们只需要存放元素值时,就选择实现Collection
接口的集合,需要保证元素唯一时选择实现 Set
接口的集合比如 TreeSet
或 HashSet
,不需要就选择实现 List
接口的比如 ArrayList
或 LinkedList
,然后再根据实现这些接口的集合的特点来选用。
为什么要使用集合?
当我们需要保存一组类型相同的数据的时候,我们应该是用一个容器来保存,这个容器就是数组,但是,使用数组存储对象具有一定的弊端, 因为我们在实际开发中,存储的数据的类型是多种多样的,于是,就出现了“集合”,集合同样也是用来存储多个数据的。
数组的缺点是一旦声明之后,长度就不可变了;同时,声明数组时的数据类型也决定了该数组存储的数据的类型;而且,数组存储的数据是有序的、可重复的,特点单一。 但是集合提高了数据存储的灵活性,Java 集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据。
Arraylist 和 Vector 的区别?
ArrayList
是List
的主要实现类,底层使用Object[ ]
存储,适用于频繁的查找工作,线程不安全 ;Vector
是List
的古老实现类,底层使用Object[ ]
存储,线程安全的。
Arraylist 与 LinkedList 区别?
是否保证线程安全:
ArrayList
和LinkedList
都是不同步的,也就是不保证线程安全;底层数据结构:
Arraylist
底层使用的是Object
数组;LinkedList
底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)-
插入和删除是否受元素位置的影响:
ArrayList
采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)
方法的时候,ArrayList
会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element)
)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。LinkedList
采用链表存储,所以,如果是在头尾插入或者删除元素不受元素位置的影响(add(E e)
、addFirst(E e)
、addLast(E e)
、removeFirst()
、removeLast()
),时间复杂度为 O(1),如果是要在指定位置i
插入和删除元素的话(add(int index, E element)
,remove(Object o)
), 时间复杂度为 O(n) ,因为需要先移动到指定位置再插入。
是否支持快速随机访问:
LinkedList
不支持高效的随机元素访问,而ArrayList
支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)
方法)。内存空间占用:
ArrayList
的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
我们在项目中一般是不会使用到 LinkedList
的,需要用到 LinkedList
的场景几乎都可以使用 ArrayList
来代替,并且,性能通常会更好!
Set
HashSet
(无序,唯一): 基于HashMap
实现的,底层采用HashMap
来保存元素LinkedHashSet
:LinkedHashSet
是HashSet
的子类,并且其内部是通过LinkedHashMap
来实现的。有点类似于我们之前说的LinkedHashMap
其内部是基于HashMap
实现一样,不过还是有一点点区别的TreeSet
(有序,唯一): 红黑树(自平衡的排序二叉树)
comparable 和 Comparator 的区别?
comparable
接口实际上是出自java.lang
包 它有一个compareTo(Object obj)
方法用来排序comparator
接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)
方法用来排序
一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()
方法或compare()
方法,当我们需要对某一个集合实现两种排序方式,比如一个 song 对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo()
方法和使用自制的Comparator
方法或者以两个 Comparator 来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 Collections.sort()
.
无序性和不可重复性的含义是什么?
1、什么是无序性?无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。
2、什么是不可重复性?不可重复性是指添加的元素按照 equals()判断时 ,返回 false,需要同时重写 equals()方法和 HashCode()方法。
比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
HashSet
、LinkedHashSet
和TreeSet
都是Set
接口的实现类,都能保证元素唯一,并且都不是线程安全的。HashSet
、LinkedHashSet
和TreeSet
的主要区别在于底层数据结构不同。HashSet
的底层数据结构是哈希表(基于HashMap
实现)。LinkedHashSet
的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet
底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。底层数据结构不同又导致这三者的应用场景不同。
HashSet
用于不需要保证元素插入和取出顺序的场景,LinkedHashSet
用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet
用于支持对元素自定义排序规则的场景。
Queue
PriorityQueue
:Object[]
数组来实现二叉堆ArrayQueue
:Object[]
数组 + 双指针
Queue 与 Deque 的区别?
Queue
是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则。
Queue
扩展了 Collection
的接口,根据 因为容量问题而导致操作失败后处理方式的不同 可以分为两类方法: 一种在操作失败后会抛出异常,另一种则会返回特殊值。
Queue 接口 |
抛出异常 | 返回特殊值 |
---|---|---|
插入队尾 | add(E e) | offer(E e) |
删除队首 | remove() | poll() |
查询队首元素 | element() | peek() |
Deque
是双端队列,在队列的两端均可以插入或删除元素。
Deque
扩展了 Queue
的接口, 增加了在队首和队尾进行插入和删除的方法,同样根据失败后处理方式的不同分为两类:
Deque 接口 |
抛出异常 | 返回特殊值 |
---|---|---|
插入队首 | addFirst(E e) | offerFirst(E e) |
插入队尾 | addLast(E e) | offerLast(E e) |
删除队首 | removeFirst() | pollFirst() |
删除队尾 | removeLast() | pollLast() |
查询队首元素 | getFirst() | peekFirst() |
查询队尾元素 | getLast() | peekLast() |
事实上,Deque
还提供有 push()
和 pop()
等其他方法,可用于模拟栈。
Map
HashMap
: JDK1.8 之前HashMap
由数组+链表组成的,数组是HashMap
的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间LinkedHashMap
:LinkedHashMap
继承自HashMap
,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap
在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。Hashtable
: 数组+链表组成的,数组是Hashtable
的主体,链表则是主要为了解决哈希冲突而存在的TreeMap
: 红黑树(自平衡的排序二叉树)
HashMap 和 Hashtable 的区别
线程是否安全:
HashMap
是非线程安全的,Hashtable
是线程安全的,因为Hashtable
内部的方法基本都经过synchronized
修饰。(如果要保证线程安全的话就使用ConcurrentHashMap
吧!)效率: 因为线程安全的问题,
HashMap
要比Hashtable
效率高一点。另外,Hashtable
基本被淘汰,不要在代码中使用它;对 Null key 和 Null value 的支持:
HashMap
可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出NullPointerException
。初始容量大小和每次扩充容量大小的不同 : ① 创建时如果不指定容量初始值,
Hashtable
默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而HashMap
会将其扩充为 2 的幂次方大小(HashMap
中的tableSizeFor()
方法保证,下面给出了源代码)。也就是说HashMap
总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。底层数据结构: JDK1.8 以后的
HashMap
在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
HashMap 和 HashSet 区别
如果你看过 HashSet
源码的话就应该知道:HashSet
底层就是基于 HashMap
实现的。(HashSet
的源码非常非常少,因为除了 clone()
、writeObject()
、readObject()
是 HashSet
自己不得不实现之外,其他方法都是直接调用 HashMap
中的方法。
HashMap |
HashSet |
---|---|
实现了 Map 接口 |
实现 Set 接口 |
存储键值对 | 仅存储对象 |
调用 put() 向 map 中添加元素 |
调用 add() 方法向 Set 中添加元素 |
HashMap 使用键(Key)计算 hashcode
|
HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals() 方法用来判断对象的相等性 |
HashMap 和 TreeMap 区别
TreeMap
和HashMap
都继承自AbstractMap
,但是需要注意的是TreeMap
它还实现了NavigableMap
接口和SortedMap
接口。
实现 NavigableMap
接口让 TreeMap
有了对集合内元素的搜索的能力。
实现SortedMap
接口让 TreeMap
有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。
hashCode()
与 equals()
的相关规定:
如果两个对象相等,则
hashcode
一定也是相同的两个对象相等,对两个
equals()
方法返回 true两个对象有相同的
hashcode
值,它们也不一定是相等的综上,
equals()
方法被覆盖过,则hashCode()
方法也必须被覆盖hashCode()
的默认行为是对堆上的对象产生独特值。如果没有重写hashCode()
,则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
==与 equals 的区别
对于基本类型来说,== 比较的是值是否相等;
对于引用类型来说,== 比较的是两个引用是否指向同一个对象地址(两者在内存中存放的地址(堆内存地址)是否指向同一个地方);
对于引用类型(包括包装类型)来说,equals 如果没有被重写,对比它们的地址是否相等;如果 equals()方法被重写(例如 String),则比较的是地址里的内容。
HashMap 多线程操作导致死循环问题
主要原因在于并发下的 Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。
ConcurrentHashMap 和 Hashtable 的区别
ConcurrentHashMap
和 Hashtable
的区别主要体现在实现线程安全的方式上不同。
底层数据结构: JDK1.7 的
ConcurrentHashMap
底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8
的结构一样,数组+链表/红黑二叉树。Hashtable
和 JDK1.8 之前的HashMap
的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;实现线程安全的方式(重要): ① 在 JDK1.7 的时候,
ConcurrentHashMap
(分段锁) 对整个桶数组进行了分割分段(Segment
),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment
的概念,而是直接用Node
数组+链表+红黑树的数据结构来实现,并发控制使用synchronized
和 CAS 来操作。(JDK1.6 以后 对synchronized
锁做了很多优化) 整个看起来就像是优化过且线程安全的HashMap
,虽然在 JDK1.8 中还能看到Segment
的数据结构,但是已经简化了属性,只是为了兼容旧版本;②Hashtable
(同一把锁) :使用synchronized
来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
Collections 工具类常用方法:
排序
查找,替换操作
同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合)
排序操作
1 void reverse(List list)//反转
2 void shuffle(List list)//随机排序
3 void sort(List list)//按自然排序的升序排序
4 void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑
5 void swap(List list, int i , int j)//交换两个索引位置的元素
6 void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面
查找,替换操作
1 int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的
2 int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll)
3 int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int 6 min(Collection coll, Comparator c)
4 void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素
5 int frequency(Collection c, Object o)//统计元素出现次数
6 int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target)
7 boolean replaceAll(List list, Object oldVal, Object newVal)//用新元素替换旧元素
同步控制
Collections
提供了多个synchronizedXxx()
方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。
我们知道 HashSet
,TreeSet
,ArrayList
,LinkedList
,HashMap
,TreeMap
都是线程不安全的。Collections
提供了多个静态方法可以把他们包装成线程同步的集合。
最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。
方法如下:
1 synchronizedCollection(Collection<T> c) //返回指定 collection 支持的同步(线程安全的)collection。
2 synchronizedList(List<T> list)//返回指定列表支持的同步(线程安全的)List。
3 synchronizedMap(Map<K,V> m) //返回由指定映射支持的同步(线程安全的)Map。
4 synchronizedSet(Set<T> s) //返回指定 set 支持的同步(线程安全的)set。
集合注意事项
集合判空
判断所有集合内部的元素是否为空,使用
isEmpty()
方法,而不是size()==0
的方式。
这是因为 isEmpty()
方法的可读性更好,并且时间复杂度为 O(1)。
集合遍历
不要在 foreach 循环里进行元素的
remove/add
操作。remove 元素请使用Iterator
方式,如果并发操作,需要对Iterator
对象加锁。
通过反编译你会发现 foreach 语法糖底层其实还是依赖 Iterator
。不过, remove/add
操作直接调用的是集合自己的方法,而不是 Iterator
的 remove/add
方法
这就导致 Iterator
莫名其妙地发现自己有元素被 remove/add
,然后,它就会抛出一个 ConcurrentModificationException
来提示用户发生了并发修改异常。这就是单线程状态下产生的 fail-fast 机制。
fail-fast 机制 :多个线程对 fail-fast 集合进行修改的时候,可能会抛出
ConcurrentModificationException
。 即使是单线程下也有可能会出现这种情况,上面已经提到过。
Java8 开始,可以使用 Collection#removeIf()
方法删除满足特定条件的元素,如
List<Integer> list = new ArrayList<>();
for (int i = 1; i <= 10; ++i) {
list.add(i);
}
list.removeIf(filter -> filter % 2 == 0); /* 删除list中的所有偶数 */
System.out.println(list); /* [1, 3, 5, 7, 9] */
集合去重
可以利用
Set
元素唯一的特性,可以快速对一个集合进行去重操作,避免使用List
的contains()
进行遍历去重或者判断包含操作。
集合转数组
使用集合转数组的方法,必须使用集合的
toArray(T[] array)
,传入的是类型完全一致、长度为 0 的空数组。
toArray(T[] array)
方法的参数是一个泛型数组,如果 toArray
方法中没有传递任何参数的话返回的是 Object
类 型数组。
数组转集合
使用工具类
Arrays.asList()
把数组转换成集合时,不能使用其修改集合相关的方法, 它的add/remove/clear
方法会抛出UnsupportedOperationException
异常。
注意事项:
1、Arrays.asList()
是泛型方法,传递的数组必须是对象数组,而不是基本类型
int[] myArray = {1, 2, 3};
List myList = Arrays.asList(myArray);
System.out.println(myList.size());//1
System.out.println(myList.get(0));//数组地址值
System.out.println(myList.get(1));//报错:ArrayIndexOutOfBoundsException
int[] array = (int[]) myList.get(0);
System.out.println(array[0]);//1
当传入一个原生数据类型数组时,Arrays.asList()
的真正得到的参数就不是数组中的元素,而是数组对象本身!此时 List
的唯一元素就是这个数组,这也就解释了上面的代码。
我们使用包装类型数组就可以解决这个问题。
Integer[] myArray = {1, 2, 3};
2、使用集合的修改方法: add()
、remove()
、clear()
会抛出异常。
List myList = Arrays.asList(1, 2, 3);
myList.add(4);//运行时报错:UnsupportedOperationException
myList.remove(1);//运行时报错:UnsupportedOperationException
myList.clear();//运行时报错:UnsupportedOperationException
Arrays.asList()
方法返回的并不是 java.util.ArrayList
,而是 java.util.Arrays
的一个内部类,这个内部类并没有实现集合的修改方法或者说并没有重写这些方法。
List myList = Arrays.asList(1, 2, 3);
System.out.println(myList.getClass());//class java.util.Arrays$ArrayList
正确的将数组转换为 ArrayList
1、手动实现工具类(目测较麻烦)
2、最简便的方法
List list = new ArrayList<>(Arrays.asList("a", "b", "c"))
3、使用 Java8 的 Stream
(推荐)
Integer [] myArray = { 1, 2, 3 };
List myList = Arrays.stream(myArray).collect(Collectors.toList());
//基本类型也可以实现转换(依赖boxed的装箱操作)
int [] myArray2 = { 1, 2, 3 };
List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
4、使用 Guava(目测也麻烦)
5、使用 Apache Commons Collections
List<String> list = new ArrayList<String>();
CollectionUtils.addAll(list, str);
6、 使用 Java9 的 List.of()
方法
Integer[] array = {1, 2, 3};
List<Integer> list = List.of(array);
------ 该笔记总结自JavaGuide,仅作为学习笔记记录 ------