java基础
1.解释下什什么是面向对象?面向对象和面向过程的区别?
面向对象是一种基于面向过程的编程思想,由执行者变为指挥者,面向对象的编程是以对象为中心,以消息为驱动。
区别:
(1)编程思路不同:面向过程以实现功能的函数开发为主,而面向对象要首先抽象出类、属性及其方法,然后通过实例化类、执行方法来完成功能。
(2)封装性:都具有封装性,但是面向过程是封装的是功能,而面向对象封装的是数据和功能。
(3)面向对象具有继承性和多态性,而面向过程没有继承性和多态性,所以面向对象优势很明显
2.面向对象的三大特性?分别解释下?
(1)封装:通常认为封装是把数据和操作数据的方法封装起来,对数据的访问只能通过已定义的接口。 (2)继 承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类/基类),得到继承信息 的被称为子类(派⽣生类)。 (3)多态:分为编译时多态(方法重载)和运行时多态(方法重写)。要实现多态需 要做两件事:一是子类继承父类并重写父类中的方法,二是用父类型引⽤子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为。
几点补充
1)子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法 访问,只是拥有。因为在一个子类被创建的时候,首先会在内存中创建一个父类对象,然后在父类对象外部放上子 类独有的属性,两者合起来形成一个子类的对象; (2)子类可以拥有自己属性和方法; (3)⼦子类可以⽤自己的方式实现父类的方法。(重写)
3.JDK、JRE、JVM 三者之间的关系?
JDK(Java Development Kit):是 Java 开发工具包,是整个 Java 的核心,包括了 Java 运⾏环境 JRE、Java 工具 和 Java 基础类库。
JRE( Java Runtime Environment):是 Java 的运行环境,包含 JVM 标准实现及 Java 核心类 库。
JVM(Java Virtual Machine):是 Java 虚拟机,是整个 Java 实现跨平台的最核心的部分,能够运行以 Java 语言 写作的软件程序。所有的 Java 程序会首先被编译为 .class 的类文件,这种类文件可以在虚拟机上执行。
4.重载和重写的区别?
(1)重载:编译时多态、同一个类中同名的方法具有不同的参数列表、不能根据返回类型进行区分【因为:函数 调用时不能指定类型信息,编译器不知道你要调哪个函数】;
(2)重写(又名覆盖):运行时多态、子类与父类之间、子类重写父类的方法具有相同的返回类型、更好的访问权限。
5.Java 中是否可以重写一个 private 或者 static 方法?
Java 中 static 方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而 static 方法是编译时静态绑定的。
static 方法跟类的任何实例都不相关,所以概念上不适用。
Java 中也不可以覆盖 private 的方法,因为 private 修饰的变量和方法只能在当前类中使用, 如果是其他的类继承当前类是不能访问到 private 变量或方法的,当然也不能覆盖。
静态方法补充:静态的方法可以被继承,但是不能重写。如果父类和子类中存在同样名称和参数的静态方法,那么该子类的方法会把原来继承过来的父类的方法隐藏,而不是重写。通俗的讲就是父类的方法和子类 的方法是两个没有关系的方法,具体调用哪一个方法是看是哪个对象的引用;这种父子类方法也不在存在多态的性质。
6.构造器是否可以被重写?
在讲继承的时候我们就知道父类的私有属性和构造方法并不能被继承,所以 Constructor 也就不不能被Override(重写),但是可以 Overload(重载),所以你可以看到一个类中有多个构造函数的情况。
7.构造方法有哪些特性?
(1)名字与类名相同;
(2)没有返回值,但不能用 void 声明构造函数;
(3)成类的对象时自动执行,无需调用。
8.在Java 中定义一个不做事且没有参数的构造方法有什么作用?
Java 程序在执行子类的构造方法之前,如果没有用 super() 来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。
因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super() 来调用⽗父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造⽅方法可供执行。解决办法是:在父类里加上一个不做事且没有参数的构造方法。
9.Java 中创建对象的⼏种方式?
1、使用 new 关键字;
2、使用 Class 类的 newInstance ⽅方法,该方法调用无参的构造器创建对象(反射):Class.forName.newInstance();
3、使用 clone() 方法;
4、反序列化,比如调用 ObjectInputStream 类的 readObject() 方法。
10.抽象类和接口有什么区别?
(1)抽象类中可以定义构造函数,接口不能定义构造函数;
(2)抽象类中可以有抽象方法和具体方法,而接口中只能有抽象方法(public abstract);
(3)抽象类中的成员权限可以是 public、默认、protected(抽象类中抽象方法就是为了重写,所以不能被 private 修饰),而接口中的成员只可以是 public(方法默认:public abstrat、成员变量量默认:public static final);
(4)抽象类中可以包含静态方法,而接口中不可以包含静态方法;
JDK 8 中的改变:
1、在 JDK1.8中,允许在接口中包含带有具体实现的方法,使用 default 修饰,这类方法就是默认方法。
2、抽象类中可以包含静态方法,在 JDK1.8 之前接口中不能包含静态方法,JDK1.8 以后可以包含。之前不能包含是因为,接口不可以实现方法,只可以定义方法,所以不能使用静态方法(因为静态方法必须实现)。现在可以包含了,只能直接用接口调用静态方法。JDK1.8 仍然不可以包含静态代码块。
11.静态变量和实例变量的区别?
静态变量:是被 static 修饰的变量,也称为类变量,它属于类,因此不管创建多少个对象,静态变量在内存中有且 仅有一个拷贝;静态变量可以实现让多个对象共享内存。
实例变量:属于某一实例,需要先创建对象,然后通过对象才能访问到它。
12.short s1 = 1;s1 = s1 + 1;有什什么错?那么 short s1 = 1; s1 += 1;呢?有没有错误?
对于 short s1 = 1; s1 = s1 + 1; 来说,在 s1 + 1 运算时会自动提升表达式的类型为 int ,那么将 int 型值赋值给short 型变量,s1 会出现类型转换错误。
对于 short s1 = 1; s1 += 1; 来说,+= 是 Java 语言规定的运算符,Java 编译器会对它进行特殊处理,因此可以正确编译。
13.Integer 和 int 的区别?
(1)int 是 Java 的八种基本数据类型之一,而 Integer 是 Java 为 int 类型提供的封装类;
(2)int 型变量的默认值是 0,Integer 变量的默认值是 null,这一点说明 Integer 可以区分出未赋值和值为 0 的 区分;
(3)Integer 变量必须实例化后才可以使用,而 int 不需要。
Integer 和 int 的⽐比较延伸:
1、由于 Integer 变量实际上是对一个 Integer 对象的引用,所以两个通过 new 生成的 Integer 变量永远是不相等 的,因为其内存地址是不同的;
2、Integer 变量和 int 变量比较时,只要两个变量的值是相等的,则结果为 true。因为包装类 Integer 和基本数据 类型 int 类型进行比较时,Java 会自动拆包装类为 int,然后进行比较,实际上就是两个 int 型变量在进行比较;
3、非 new 生成的 Integer 变量和 new Integer() 生成的变量进行比较时,结果为 false。因为非 new 生成的 Integer 变量指向的是 Java 常量池中的对象,而 new Integer() 生成的变量指向堆中新建的对象,两者在内存中的 地址不同;
4、对于两个非 new 生成的 Integer 对象进行比较时,如果两个变量的值在区间 [-128, 127] 之间,则比较结果为 true,否则为 false。Java 在编译 Integer i = 100 时,会编译成 Integer i = Integer.valueOf(100),而 Integer 类型 的 valueOf 的源码如下所示:
public static Integer valueOf(int var0) {
return var0 >= -128 && var0 <= Integer.IntegerCache.high ? Integer.IntegerCache.cache[var0 + 128] : new Integer(var0);
}
从上面的代码中可以看出:Java 对于 [-128, 127] 之间的数会进行缓存,比如:Integer i = 127,会将 127 进行缓 存,下次再写 Integer j = 127 的时候,就会直接从缓存中取出,而对于这个区间之外的数就需要 new 了。
包装类的缓存:Boolean:全部缓存 Byte:全部缓存 Character:<= 127 缓存 Short:-128 — 127 缓存 Long:-128 — 127 缓存 Integer:-128 — 127 缓存 Float:没有缓存 Doulbe:没有缓存
14、装箱和拆箱
自动装箱是 Java 编译器在基本数据类型和对应得包装类之间做的一个转化。比如:把 int 转化成 Integer,double转化成 Double 等等。反之就是 自动拆箱。 原始类型:boolean、char、byte、short、int、long、float、double 封装类型:Boolean、Character、Byte、Short、Integer、Long、Float、Double
15、switch 语句能否作用在 byte上,能否作用在 long 上,能否作用在String 上?
在 switch(expr 1) 中,expr1 只能是一个整数表达式或者枚举常量。而整数表达式可以是 int 基本数据类型或者 Integer 包装类型。由于,byte、short、char 都可以隐式转换为 int,所以,这些类型以及这些类型的包装类型也 都是可以的。而 long 和 String 类型都不符合 switch 的语法规定,并且不能被隐式的转换为 int 类型,所以,它们 不能作用于 switch 语句中。不过,需要注意的是在 JDK1.7 版本之后 switch 就可以作用在 String 上了了。
16、Object 的常用方法有哪些?
clone 方法:用于创建并返回当前对象的一份拷贝;
getClass 方法:用于返回当前运行时对象的 Class;
toString 方法:返回对象的字符串表示形式;
finalize 方法:实例被垃圾回收器回收时触发的方法;
equals 方法:用于比较两个对象的内存地址是否相等,一般需要重写;
hashCode 方法:用于返回对象的哈希值;
notify 方法:唤醒一个在此对象监视器上等待的线程。如果有多个线程在等待只会唤醒⼀一个。
notifyAll 方法:作用跟 notify() 一样,只不过会唤醒在此对象监视器上等待的所有线程,而不不是一个线程。
wait 方法:让当前对象等待;.......
16、final、finally、finalize 的区别?
final:⽤于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、被其修饰的类不可继承; finally:异常处理语句结构的一部分,表示总是执行; finallize:Object类的一个方法,在垃圾回收时会调用被回收对象的finalize
17、== 和 equals 的区别?
==:如果比较的对象是基本数据类型,则比较的是数值是否相等;如果比较的是引用数据类型,则比较的是对象的 地址值是否相等。
equals 方法:用来比较两个对象的内容是否相等。注意:equals 方法不能用于比较基本数据类型的变量。如果没 有对 equals 方法进行重写,则比较的是引用类型的变量所指向的对象的地址(很多类重写了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等)。
18、两个对象的 hashCode() 相同,则 equals() 也一定为 true 吗?
两个对象的 hashCode() 相同,equals() 不一定为 true。因为在散列表中,hashCode() 相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等【散列列冲突】。
19、为什么重写 equals() 就一定要重写 hashCode() 方法?
这个问题应该是有个前提,就是你需要用到 HashMap、HashSet 等 Java 集合,用不到哈希表的话,其实仅重写 equals() 方法也可以。而工作中的场景是常常用到 Java 集合,所以 Java 官方建议重写 equals() 就一定要重写 hashCode() 方法。
对于对象集合的判重,如果一个集合含有 10000 个对象实例,仅仅使用 equals() 方法的话,那么对于一个对象判 重就需要比较 10000 次,随着集合规模的增大,时间开销是很大的。但是同时使用哈希表的话,就能快速定位到 对象的大概存储位置,并且在定位到大概存储位置后,后续比较过程中,如果两个对象的 hashCode 不相同,也不再需要调用 equals() 方法,从而⼤大减少了 equals() 的比较次数。
所以从程序实现原理上来讲的话,既需要 equals() 方法,也需要 hashCode() 方法。那么既然重写了 equals(),那 么也要重写 hashCode() ⽅方法,以保证两者之间的配合关系。
hashCode()与equals()的相关规定:
1、如果两个对象相等,则 hashCode 一定也是相同的;
2、两个对象相等,对两个对象分别调用 equals 方法都返回 true;
3、两个对象有相同的 hashCode 值,它们也不一定是相等的;
4、因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖;
5、hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论 如何都不会相等(即使这两个对象指向相同的数据)。
20、& 和 && 的区别?
Java 中 && 和 & 都是表示与的逻辑运算符,都表示逻辑运输符 and,当两边的表达式都为 true 的时候,整个运算 结果才为 true,否则为 false。
&&:有短路功能,当第一个表达式的值为 false 的时候,则不再计算第⼆个表达式;
&:不管第一个表达式结果是否为 true,第二个都会执行。除此之外,& 还可以用作位运算符:当 & 两边的表达式 不是 Boolean 类型的时候,& 表示按位操作。
21.Java 中的参数传递时传值呢?还是传引用?
Java 的参数是以值传递的形式传入方法中,而不是引用传递。
当传递方法参数类型为基本数据类型(数字以及布尔值)时,一个方法是不可能修改一个基本数据类型的参数。
当传递方法参数类型为引用数据类型时,一个方法将修改一个引用数据类型的参数所指向对象的值。即使 Java 函 数在传递引用数据类型时,也只是拷贝了引用的值罢了,之所以能修改引用数据是因为它们同时指向了一个对象, 但这仍然是按值调⽤而不是引用调用。
22、Java 中的 Math.round(-1.5) 等于多少?
等于 -1,因为在数轴上取值时,中间值(0.5)向右取整,所以正 0.5 是往上取整,负 0.5 是直接舍弃。
23、两个二进制数的异或结果是什么?
两个二进制数异或结果是这两个二进制数差的绝对值。表达式如下:a^b = |a-b|。
两个二进制 a 与 b 异或,即 a 和 b 两个数按位进行运算。如果对应的位相同,则为 0(相当于对应的算术相减), 如果不同即为 1(相当于对应的算术相加)。由于二进制每个位只有两种状态,要么是 0,要么是 1,则按位异或 操作可表达为按位相减取值相对值,再按位累加。
26、如何实现对象的克隆?
(1)实现 Cloneable 接口并重写 Object 类中的 clone() ⽅方法;
(2)实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深克隆。
27、深克隆和浅克隆的区别?
(1)浅克隆:拷贝对象和原始对象的引用类型引用同⼀一个对象。浅克隆只是复制了对象的引用地址,两个对象指 向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化,这就是浅克隆。
(2)深克隆:拷贝对象和原始对象的引用类型引用不同对象。深拷贝是将对象及值复制过来,两个对象修改其中 任意的值另一个值不会改变,这就是深拷贝(例例:JSON.parse() 和 JSON.stringify(),但是此方法无法复制函数类 型)。
补充:
深克隆的实现就是在引用类型所在的类实现 Cloneable 接⼝口,并使⽤用 public 访问修饰符重写 clone 方法。
Java 中定义的 clone 没有深浅之分,都是统一的调用 Object 的 clone 方法。为什么会有深克隆的概念?是由于我们在实现的过程中刻意的嵌套了 clone 方法的调用。也就是说深克隆就是在需要克隆的对象类型的类中重新实现克隆方法 clone()。
28、什么是 Java 的序列化,如何实现 Java 的序列化?
对象序列化是一个用于将对象状态转换为字节流的过程,可以将其保存到磁盘文件中或通过⽹络发送到任何其他程序。从字节流创建对象的相反的过程称为反序列化。而创建的字节流是与平台无关的,在一个平台上序列化的对象 可以在不同的平台上反序列化。序列化是为了解决在对象流进行读写操作时所引发的问题。
序列化的实现:将需要被序列化的类实现 Serializable 接口,该接口没有需要实现的方法,只是用于标注该对象是 可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造⼀个 ObjectOutputStream 对象,接着使用 ObjectOutputStream 对象的 writeObject(Object obj) 方法可以将参数为 obj 的对象写出,要恢复的话则使用 输入流。
29、什么情况下需要序列化?
(1)当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
(2)当你想用套接字在网络上传送对象的时候;
(3)当你想通过 RMI 传输对象的时候。
30.初始化考察,指出程序运行结果
public class InitialTest {
public static void main(String[] args) {
A ab = new B();
ab = new B();
}
}
class A {
static { // 父类静态代码块
System.out.print("A");
}
public A() { // 父类构造器
System.out.print("a");
}
}
class B extends A {
static { // 子类静态代码块
System.out.print("B");
}
public B() { // 子类构造器
System.out.print("b");
}
}
执行结果:ABabab,两个考察点:
1)静态变量只会初始化(执行)一次。
2)当有父类时,完整的初始化顺序为:父类静态变量(静态代码块)->子类静态变量(静态代码块)->父类非静态变量(非静态代码块)->父类构造器 ->子类非静态变量(非静态代码块)->子类构造器 。
关于初始化,这题算入门题,我之前还写过一道有(fei)点(chang)意(bian)思(tai)的进阶题目,有兴趣的可以看看:一道有意思的“初始化”面试题
31.Error 和 Exception 有什么区别?
Error 和 Exception 都是 Throwable 的子类,用于表示程序出现了不正常的情况。区别在于:
Error 表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题,比如内存溢出,不可能指望程序能处理这样的情况。
Exception 表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题,也就是说,它表示如果程序运行正常,从不会发生的情况
32.try、catch、finally 考察,请指出下面程序的运行结果
public class TryDemo {
public static void main(String[] args) {
System.out.println(test());
}
public static int test() {
try {
return 1;
} catch (Exception e) {
return 2;
} finally {
System.out.print("3");
}
}
}
try、catch。finally 的基础用法,在 return 前会先执行 finally 语句块,所以是先输出 finally 里的 3,再输出 return 的 1。
33.try、catch、finally 考察3,请指出下面程序的运行结果。
public class TryDemo {
public static void main(String[] args) {
System.out.println(test1());
}
public static int test1() {
int i = 0;
try {
i = 2;
return i;
} finally {
i = 3;
}
}
}
执行结果:2。
这边估计有不少同学会以为结果应该是 3,因为我们知道在 return 前会执行 finally,而 i 在 finally 中被修改为 3 了,那最终返回 i 不是应该为 3 吗?确实很容易这么想,我最初也是这么想的,当初的自己还是太年轻了啊。
这边的根本原因是,在执行 finally 之前,JVM 会先将 i 的结果暂存起来,然后 finally 执行完毕后,会返回之前暂存的结果,而不是返回 i,所以即使这边 i 已经被修改为 3,最终返回的还是之前暂存起来的结果 2。
这边其实根据字节码可以很容易看出来,在进入 finally 之前,JVM 会使用 iload、istore 两个指令,将结果暂存,在最终返回时在通过 iload、ireturn 指令返回暂存的结果
JAVA泛型与反射
1.Java 的泛型是如何工作的 ? 什么是类型擦除 ?
泛型是通过类型擦除来实现的,编译器器在编译时擦除了所有类型相关的信息,所以在运行时不不存在任何类型相关的 信息。例如:List<String> 在运行时仅用一个 List 来表示。这样做的目的,是确保能和 Java 5 之前的版本开发二进 制类库进行兼容。
类型擦除:泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 < T > 则会被转译成普通 的 Object 类型,如果指定了上限如 < T extends String > 则类型参数就被替换成类型上限。
补充
List<String> list = new ArrayList<String>();
1、两个 String 其实只有第一个起作用,后⾯面一个没什么卵用,只不过 JDK7 才开始支持 List<String>list = new ArrayList<> 这种写法。
2、第⼀个 String 就是告诉编译器器,List 中存储的是 String 对象,也就是起类型检查的作⽤用,之后编译器会擦除泛 型占位符,以保证兼容以前的代码。
2.什么是泛型中的限定通配符和非限定通配符 ?
限定通配符对类型进行了限制。有两种限定通配符,一种是< ? extends T > 它通过确保类型必须是 T 的子类来设定 类型的上界,另一种是< ? super T >它通过确保类型必须是 T 的父类来设定类型的下界。泛型类型必须用限定内的 类型来进行初始化,否则会导致编译错误。另一方面 < ? > 表示了非限定通配符,因为 < ? > 可以用任意类型来替 代。
3、List<? extends T> 和 List <? super T> 之间有什什么区别 ?
这两个 List 的声明都是限定通配符的例子,List< ? extends T > 可以接受任何继承自 T 的类型的 List,而List < ? super T > 可以接受任何 T 的父类构成的 List。例如 List< ? extends Number > 可以接受 List< Integer > 或 List< Float > 。
Array 不支持泛型,要用 List 代替 Array,因为 List 可以提供编译器的类型安全保证,而 Array却不不能。
4、Java 中的反射是什么意思?有哪些应用场景?
每个类都有一个 Class 对象,包含了与类有关的信息。当编译⼀个新类时,会产生一个同名的 .class 文件,该文件 内容保存着 Class 对象。类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 Class.forName("com.mysql.jdbc.Driver") 这种方式来控制类的加载,该方法会返回一个 Class 对象。
反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以 加载进来。Class 和 java.lang.reflect ⼀起对反射提供了支持,java.lang.reflect 类库主要包含了了以下三个类:
(1)Field :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
(2)Method :可以使用 invoke() 方法调用与 Method 对象关联的方法;
(3)Constructor :可以用 Constructor 创建新的对象。 应用举例:⼯厂模式,使用反射机制,根据全限定类名获得某个类的 Class 实例。
5、反射的优缺点?
优点:
运行期类型的判断,class.forName() 动态加载类,提高代码的灵活度;
缺点:
尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时, 下⾯几条内容应该牢记于心。
(1)性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那 些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
(2)安全限制 :使⽤用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制 的环境中运行,如 Applet,那么这就是个问题了了。
(3)内部暴露:由于反射允许代码执行一些在正常情况下不被允许的操作(比如:访问私有的属性和方法),所 以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了了抽象性,因 此当平台发生改变的时候,代码的行为就有可能也随着变化。
6、Java 中的动态代理是什么?有哪些应用?
动态代理:当想要给实现了某个接口的类中的方法,加一些额外的处理。比如说加日志,加事务等。可以给这个类 创建一个代理,故名思议就是创建一个新的类,这个类不仅包含原来类⽅方法的功能,而且还在原来的基础上添加了 额处理的新功能。这个代理类并不是定义好的,是动态生成的。具有解耦意义,灵活,扩展性强。
动态代理的应用:Spring 的 AOP 、加事务、加权限、加⽇志。
7、怎么实现动态代理?
首先必须定义一个接口,还要有一个 InvocationHandler(将实现接口的类的对象传递给它)处理类。再有一个工 具类 Proxy(习惯性将其称为代理类,因为调⽤它的 newInstance() 可以产生代理对象,其实它只是一个产生代理 对象的工具类)。利用到 InvocationHandler,拼接代理理类源码,将其编译生成代理理类的二进制码,利用加载器加 载,并将其实例化产生代理对象,最后返回。
每一个动态代理类都必须要实现 InvocationHandler 这个接口,并且每个代理类的实例都关联到了了一个 handler, 当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由 InvocationHandler 这个接口的 invoke ⽅法来进行调用。我们来看 InvocationHandler 这个接口的唯⼀一个方法 invoke 方法:
Object invoke(Object proxy, Method method, Object[] args) throws Throwable
proxy: 指代我们所代理的那个真实对象
method: 指代的是我们所要调用真实对象的某个方法的 Method 对象
args: 指代的是调用真实对象某个方法时接受的参数
Proxy 类的作用是动态创建一个代理对象的类。它提供了许多的方法,但是我们用的最多的就是
newProxyInstance 这个⽅法:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler) throws IllegalArgumentException
loader:一个 ClassLoader 对象,定义了由哪个 ClassLoader 对象来对生成的代理对象进行加载;
interfaces:一个 Interface 对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了了一 组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
handler:一个 InvocationHandler 对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler 对象上。
通过 Proxy.newProxyInstance 创建的代理对象是在 Jvm 运行时动态⽣生成的一个对象,它并不不是我们的InvocationHandler 类型,也不是我们定义的那组接口的类型,而是在运行是动态生成的一个对象。
java字符串
1、字节和字符的区别?
字节是存储容量量的基本单位; 字符是数字、字母、汉字以及其他语言的各种符号;
1 字节 = 8 个二进制单位,一个字符由一个字节或多个字节的二进制单位组成。
2、String 为什么要设计为不可变类?
在 Java 中将 String 设计成不可变的是综合考虑到各种因素的结果。主要的原因主要有以下三点:
(1)字符串常量池的需要:字符串常量池是 Java 堆内存中一个特殊的存储区域, 当创建一个 String 对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象;
(2)允许 String 对象缓存 HashCode:Java 中 String 对象的哈希码被频繁地使用, 比如在HashMap 等容器中。 字符串不变性保证了 hash 码的唯一性,因此可以放心地进行缓存。这也是一种性能优化⼿手段,意味着不必每次都 去计算新的哈希码;
(3)String 被许多的 Java 类(库)用来当做参数,例如:网络连接地址 URL、文件路径 path、还有反射机制所需要 的String 参数等, 假若 String 不是固定不变的,将会引起各种安全隐患。
3、String、StringBuilder、StringBuffer 的区别?
String:用于字符串操作,属于不可变类;【补充:String 不是基本数据类型,是引用类型,底层用 char 数组实现 的】
StringBuilder:与 StringBuffer 类似,都是字符串缓冲区,但线程不安全; StringBuffer:也用于字符串操作,不同之处是 StringBuffer 属于可变类,对方法加了了同步锁,线程安全 StringBuffer的补充
说明:StringBuffer 中并不是所有方法都使用了 Synchronized 修饰来实现同步:
@Overridepublic StringBuffer insert(int dstOffset, CharSequence s) { // Note, synchronization achieved via invocations of other StringBuffer methods // after narrowing of s to specific type // Ditto for toStringCache clearing super.insert(dstOffset, s); return this; } |
执⾏行行效率:StringBuilder > StringBuffer > String
4、String 字符串修改实现的原理理?
当用 String 类型来对字符串进行修改时,其实现方法是首先创建一个 StringBuffer,其次调用 StringBuffer的append() 方法,最后调用 StringBuffer 的 toString() 方法把结果返回。
5、String str = "i" 与 String str = new String("i") 一样吗?
不一样,因为内存的分配方式不一样。String str = "i" 的方式,Java 虚拟机会将其分配到常量量池中;而 String str = new String("i") 则会被分到堆内存中。
在执行 String str1 = "abc" 的时候,JVM 会首先检查字符串常量池中是否已经存在该字符串对象,如果已经存在, 那么就不会再创建了,直接返回该字符串在字符串常量池中的内存地址;如果该字符串还不存在字符串常量池中, 那么就会在字符串常量池中创建该字符串对象,然后再返回。所以在执行 String str2 = "abc" 的时候,因为字符串 常量池中已经存在“abc”字符串对象了,就不会在字符串常量池中再次创建了,所以栈内存中 str1 和 str2 的内存地 址都是指向 "abc" 在字符串常量池中的位置,所以 str1 = str2 的运行结果为 true。
而在执行 String str3 = new String("abc") 的时候,JVM 会首先检查字符串常量池中是否已经存在“abc”字符串,如果已经存在,则不会在字符串常量池中再创建了;如果不存在,则就会在字符串串常量池中创建 "abc" 字符串对象, 然后再到堆内存中再创建一份字符串对象,把字符串常量池中的 "abc" 字符串内容拷贝到内存中的字符串对象中, 然后返回堆内存中该字符串的内存地址,即栈内存中存储的地址是堆内存中对象的内存地址。String str4 = new String("abc") 是在堆内存中又创建了一个对象,所以 str 3 == str4 运行的结果是 false。str1、str2、str3、str4 在 内存中的存储状况如下图所示:
6、String 类的常用方法都有那些?
indexOf():返回指定字符的索引。
charAt():返回指定索引处的字符。
replace():字符串串替换。
trim():去除字符串串两端空⽩白。
split():分割字符串返回一个分割后的字符串数组。
getBytes():返回字符串的 byte 类型数组。 length():返回字符串长度。
toLowerCase():将字符串转成小写字母。 toUpperCase():将字符串转成大写字符。 substring():截取字符串串。
equals():字符串比较。
7、final 修饰 StringBuffer 后还可以 append 吗?
可以。final 修饰的是⼀一个引用变量,那么这个引用始终只能指向这个对象,但是这个对象内部的属性是可以变化 的。
官⽅方⽂文档解释:once a final variable has been assigned, it always contains the same value. If a final variable holds a reference to an object, then the state of the object may be changed by operations on the object, but the variable will always refer to the same object.
JAVA异常
1、finally 块中的代码什么时候被执行?
在 Java 语言的异常处理中,finally 块的作用就是为了保证无论出现什么情况,finally 块里的代码一定会被执行。由于程序执行 return 就意味着结束对当前函数的调用并跳出这个函数体,因此任何语句要执行都只能在return 前执行(除非碰到 exit 函数),因此 finally 块里的代码也是在 return 之前执行的。
此外,如果 try-finally 或者 catch-finally 中都有 return,那么 finally 块中的 return 将会覆盖别处的 return 语句,最终返回到调用者那里的是 finally 中 return 的值。
2、finally 是不是一定会被执行到?
不一定。下面列举两种执行不到的情况:
(1)当程序进入 try 块之前就出现异常时,会直接结束,不会执行 finally 块中的代码;
(2)当程序在 try 块中强制退出时也不会去执行 finally 块中的代码,比如在 try 块中执行exit方法。
3、try-catch-finally 中,如果 catch 中 return 了,finally 还会执行 吗?
会。程序在执行到 return 时会首先将返回值存储在⼀个指定的位置,其次去执行 finally 块,最后再返回。因此,对基本数据类型,在 finally 块中改变 return 的值没有任何影响,直接覆盖掉;而对引用类型是有影响的,返回的 是在 finally 对 前面 return 语句句返回对象的修改值。
4、try-catch-finally 中那个部分可以省略?
catch 可以省略。try 只适合处理运行时异常,try+catch 适合处理运行时异常+普通异常。也就是说,如果你只用 try 去处理普通异常却不加以 catch 处理理,编译是通不过的,因为编译器器硬性规定,普通异常如果选择捕获,则必须用 catch 显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以 catch 可以省略,你加上 catch 编译器也觉得无可厚非。
5、Error 和 Exception 的区别?
Error 类和 Exception 类的父类都是 Throwable 类。主要区别如下:
Error 类: 一般是指与虚拟机相关的问题,如:系统崩溃、虚拟机错误、内存空间不足、方法调⽤用栈溢出等。这类 错误将会导致应用程序中断,仅靠程序本身无法恢复和预防;
Exception 类:分为运行时异常和受检查的异常。
6、运行时异常与受检异常有何异同?
运行时异常:如:空指针异常、指定的类找不到、数组越界、方法传递参数错误、数据类型转换错误。可以编译通 过,但是一运行就停止了,程序不会自己处理理;
受检查异常:要么用 try … catch… 捕获,要么用 throws 声明抛出,交给父类处理。
7、throw 和 throws 的区别?
(1)throw:在方法体内部,表示抛出异常,由方法体内部的语句处理理;throw 是具体向外抛出异常的动作,所 以它抛出的是一个异常实例;
(2)throws:在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理;表示出现异常的可能性,并不一定会发生这种异常。
8、常见的异常类有哪些?
NullPointerException:当应用程序试图访问空对象时,则抛出该异常。
SQLException:提供关于数据库访问错误或其他错误信息的异常。 IndexOutOfBoundsException:指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。
FileNotFoundException:当试图打开指定路径名表示的文件失败时,抛出此异常。 IOException:当发⽣生某种 I/O 异常时,抛出此异常。此类是失败或中断的 I/O 操作生成的异常的通⽤用类。
ClassCastException:当试图将对象强制转换为不是实例的子类时,抛出该异常。 IllegalArgumentException:抛出的异常表明向方法传递了一个不不合法或不正确的参数。
9、主线程可以捕获到子线程的异常吗?
线程设计的理念:“线程的问题应该线程⾃己本身来解决,而不要委托到外部”。
正常情况下,如果不做特殊的处理,在主线程中是不能够捕获到子线程中的异常的。如果想要在主线程中捕获子线 程的异常,我们可以用如下的方式进行处理,使⽤用 Thread 的静态方法
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandle());
javaIO
1、Java 中的 IO 流的分类?说出几个你熟悉的实现类?
按功能来分:输入流(input)、输出流(output)。
按类型来分:字节流和字符流
字节流:InputStream/OutputStream 是字节流的抽象类,这两个抽象类又派生了若干子类,不不同的子类分别处理不同的操作类型。具体子类如下所示:
字符流:Reader/Writer 是字符的抽象类,这两个抽象类也派生了若干子类,不同的子类分别处理理不同的操作类型。
2、字节流和字符流有什么区别?
字节流按 8 位传输,以字节为单位输入输出数据,字符流按 16 位传输,以字符为单位输入输出数据。 但是不管文件读写还是网络发送接收,信息的最小存储单元都是字节。
3、BIO、NIO、AIO 有什什么区别?
BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机 1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考 虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是, 当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来 应对更高的并发量。
NIO:New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。NIO 是一种同步非阻塞的 I/O 模型,在 Java1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道 的 I/O 操作方法。NIO 提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程 序,可以使用同步阻塞 I/O 来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机 制。也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的 作。AIO 是异步 IO 的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。 对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作 本身是同步的。
java集合
1、Java 中常用的容器有哪些?
常见容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的 映射表
Collection
Set
1. SetTreeSet:基于红黑树实现,支持有序性操作,例如:根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
2. HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是 说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
3. LinkedHashSet:具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。
List
1. ArrayList:基于动态数组实现,支持随机访问。
2. Vector:和 ArrayList 类似,但它是线程安全的。
3. LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此, LinkedList 还可以用作栈、队列和双向队列。
Queue
1. LinkedList:可以用它来实现双向队列。
2. PriorityQueue:基于堆结构实现,可以用它来实现优先队列列。
Map
1. TreeMap:基于红黑树实现。
2. HashMap:基于哈希表实现。
3. HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写⼊入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程 安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
4. LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
2、ArrayList 和 LinkedList 的区别?
ArrayList:底层是基于数组实现的,查找快,增删较慢;
LinkedList:底层是基于链表实现的。确切的说是循环双向链表(JDK1.6 之前是双向循环链表、JDK1.7 之后取消了循环),查找慢、增删快。LinkedList 链表由一系列列表项连接而成,一个表项包含 3 个部分:元素内容、前驱表 和后驱表。链表内部有一个 header 表项,既是链表的开始也是链表的结尾。header 的后继表项是链表中的第一 个元素,header 的前驱表项是链表中的最后一个元素。
ArrayList 的增删未必就是比 LinkedList 要慢:
1. 如果增删都是在末尾来操作【每次调用的都是 remove() 和 add()】,此时 ArrayList 就不需要移动和复制数组 来进行操作了。如果数据量有百万级的时,速度是会比 LinkedList 要快的。
2. 如果删除操作的位置是在中间。由于 LinkedList 的消耗主要是在遍历上,ArrayList 的消耗主要是在移动和复制上(底层调用的是 arrayCopy() 方法,是 native 方法)。LinkedList 的遍历速度是要慢于 ArrayList 的复制 移动速度的如果数据量有百万级的时,还是 ArrayList 要快。
3、ArrayList 实现 RandomAccess 接口有何作用?为何 LinkedList 却没实现这个接口?
1. RandomAccess 接口只是⼀一个标志接口,只要 List 集合实现这个接口,就能⽀持快速随机访问。通过查看 Collections 类中的 binarySearch() 方法,可以看出,判断 List 是否实现 RandomAccess 接口来实行 indexedBinarySerach(list, key) 或 iteratorBinarySerach(list, key)方法。再通过查看这两个方法的源码发 现:实现 RandomAccess 接口的 List 集合采用一般的 for 循环遍历,而未实现这接口则采用迭代器,即 ArrayList 一般采用 for 循环遍历,而 LinkedList 一般采用迭代器遍历;
2. ArrayList 用 for 循环遍历比 iterator 迭代器遍历快,LinkedList 用 iterator 迭代器遍历比 for 循环遍历快。 所以说,当我们在做项目时,应该考虑到 List 集合的不同子类采用不同的遍历方式,能够提高性能。
4、ArrayList 的扩容机制?
推荐阅读:
1. 当使用 add 方法的时候首先调用 ensureCapacityInternal 方法,传入 size+1 进去,检查是否需要扩充elementData 数组的大小;
2. newCapacity = 扩充数组为原来的 1.5 倍(不能自定义),如果还不够,就使用它指定要扩充的⼤大小 minCapacity ,然后判断 minCapacity 是否大于 MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8) ,如果大于,就 取 Integer.MAX_VALUE;
3. 扩容的主要方法:grow;
4. ArrayList 中 copy 数组的核心就是 System.arraycopy ⽅法,将 original 数组的所有数据复制到 copy 数组 中,这是一个本地方法。
5、Array 和 ArrayList 有何区别?什么时候更适合用 Array?
1. Array 可以容纳基本类型和对象,而 ArrayList 只能容纳对象;
2. Array 是指定大小的,而 ArrayList 大小是固定的。
什么时候更适合使用 Array:
1. 如果列列表的大小已经指定,大部分情况下是存储和遍历它们;
2. 对于遍历基本数据类型,尽管 Collections 使用自动装箱来减轻编码任务,在指定大小的基本类型的列列表上工 作也会变得很慢;
3. 如果你要使用多维数组,使⽤ [ ][ ] ⽐比 List> 更容易。
6、HashMap 的实现原理/底层数据结构?JDK1.7 和 JDK1.8
HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry。
这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干
它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以CollectionssynchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap
JDK1.7:Entry数组 + 链表
JDK1.8:Node 数组 + 链表/红⿊黑树,当链表上的元素个数超过 8 个并且数组长度 >= 64 时自动转化成红⿊黑树,节 点变成树节点,以提高搜索效率和插入效率到 O(logN)。Entry 和 Node 都包含 key、value、hash、next 属性。
7、HashMap 的 put 方法的执行过程?
当我们想往一个 HashMap 中添加一对 key-value 时,系统首先会计算 key 的 hash 值,然后根据 hash 值确认在 table 中存储的位置。若该位置没有元素,则直接插入。否则迭代该处元素链表并依次比较其 key 的 hash 值。如 果两个 hash 值相等且 key 值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),则用新的 Entry 的 value 覆盖原来节点的 value。如果两个 hash 值相等但 key 值不不等 ,则将该节点插入该链表的链头。
8、HashMap 的 get 方法的执行过程?
通过 key 的 hash 值找到在 table 数组中的索引处的 Entry,然后返回该 key 对应的 value 即可。 在这里能够根据 key 快速的取到 value 除了和 HashMap 的数据结构密不可分外,还和 Entry 有莫大的关系。
HashMap 在存储过程中并没有将 key,value 分开来存储,而是当做一个整体 key-value 来处理的,这个整体就是 Entry 对象。同时 value 也只相当于 key 的附属而已。在存储的过程中,系统根据 key 的 HashCode 来决定 Entry 在 table 数组中的存储位置,在取的过程中同样根据 key 的 HashCode 取出相对应的 Entry 对象(value 就包含在里面)。
9、HashMap 的 resize 方法的执行过程?
有两种情况会调用 resize 方法:
1. 第一次调用 HashMap 的 put 方法时,会调用 resize ⽅法对 table 数组进行初始化,如果不传⼊指定值,默 认大小为 16。
2. 扩容时会调用 resize,即 size > threshold 时,table 数组大小翻倍。 每次扩容之后容量都是翻倍。扩容后要将原数组中的所有元素找到在新数组中合适的位置。
当我们把 table[i] 位置的所有 Node 迁移到 newtab 中去的时候:这里面的 node 要么在 newtab 的 i 位置(不 变),要么在 newtab 的 i + n 位置。也就是我们可以这样处理:把 table[i] 这个桶中的 node 拆分为两个链表 l1 和 l2:如果 hash & n == 0,那么当前这个 node 被连接到 l1 链表;否则连接到 l2 链表。这样下来,当遍历完 table[i] 处的所有 node 的时候,我们得到两个链表 l1 和 l2,这时我们令 newtab[i] = l1,newtab[i + n] = l2,这 就完成了了 table[i] 位置所有 node 的迁移(rehash),这也是 HashMap 中容量一定的是 2 的整数次幂带来的方便 之处。
10、HashMap 的 size 为什么必须是 2 的整数次方?
1. 这样做总是能够保证 HashMap 的底层数组长度为 2 的 n 次方。当 length 为 2 的 n 次⽅方时,h & (length - 1) 就相当于对 length 取模,而且速度比直接取模快得多,这是 HashMap 在速度上的一个优化。而且每次扩容 时都是翻倍。
2. 如果 length 为 2 的次幂,则 length - 1 转化为二进制必定是 11111……的形式,在与 h 的二进制进行与操作 时效率会非常的快,而且空间不浪费。但是,如果 length 不是 2 的次幂,比如:length 为 15,则 length - 1 为 14,对应的二进制为 1110,在于 h 与操作,最后一位都为 0 ,而 0001,0011,0101,1001,1011, 0111,1101 这⼏个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位 置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了了查询的效率,这样就会造成空间的浪费。
11、HashMap 多线程死循环问题?
详细请阅读:
HashMap多线程死循环问题_XF的专栏-CSDN博客_hashmap 多线程操作导致死循环问题
Java面试题:高并发环境下,HashMap可能出现的致命问题。注意:是在jdk8以下版本_炫的博客-CSDN博客_hashmap多线程扩容出现的问题
主要是多线程同时 put 时,如果同时触发了 rehash 操作,会导致 HashMap 中的链表中出现循环节点,进而使得 后面 get 的时候,会死循环。
12、HashMap 的 get 方法能否判断某个元素是否在 map 中?
HashMap 的 get 函数的返回值不能判断一个 key 是否包含在 map 中,因为 get 返回 null 有可能是不包含该key,也有可能该 key 对应的 value 为 null。因为 HashMap 中允许 key 为 null,也允许 value 为 null。
13、HashMap 与 HashTable 的区别是什什么?
- HashTable 基于 Dictionary 类,而 HashMap 是基于 AbstractMap。Dictionary 是任何可将键映射到相应值 的类的抽象父类,而 AbstractMap 是基于 Map 接口的实现,它以最大限度地减少实现此接口所需的工作。
- HashMap 的 key 和 value 都允许为 null,而 Hashtable 的 key 和 value 都不允许为 null。HashMap 遇到 key 为 null 的时候,调用 putForNullKey 方法进行处理,而对 value 没有处理理;Hashtable 遇到 null,直接 返回 NullPointerException。
3. Hashtable 是线程安全的,而 HashMap 不是线程安全的,但是我们也可以通过
Collections.synchronizedMap(hashMap),使其实现同步
17、HashSet 的实现原理?
HashSet 的实现是依赖于 HashMap 的,HashSet 的值都是存储在 HashMap 中的。在 HashSet 的构造法中会初始化一个 HashMap 对象,HashSet 不允许值重复。因此,HashSet 的值是作为 HashMap 的 key 存储在 HashMap 中的,当存储的值已经存在时返回 false。
18、HashSet 怎么保证元素不重复的?
public boolean add(E e) { return map.put(e, PRESENT)==null; } |
元素值作为的是 map 的 key,map 的 value 则是 PRESENT 变量,这个变量只作为放入 map 时的一个占位符而存 在,所以没什么实际用处。其实,这时候答案已经出来了了:HashMap 的 key 是不能重复的,而这里HashSet 的元素又是作为了 map 的 key,当然也不能重复了。
19、LinkedHashMap 的实现原理?
LinkedHashMap 也是基于 HashMap 实现的,不同的是它定义了一个 Entry header,这个 header 不是放在 Table 里,它是额外独立出来的。LinkedHashMap 通过继承 hashMap 中的 Entry,并添加两个属性 Entry before,after 和 header 结合起来组成一个双向链表,来实现按插入顺序或访问顺序排序。
LinkedHashMap 定义了排序模式 accessOrder,该属性为 boolean 型变量,对于访问顺序,为 true;对于插入 顺序,则为 false。一般情况下,不必指定排序模式,其迭代顺序即为默认为插入顺序。
20、Iterator 怎么使用?有什么特点?
迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结 构。迭代器通常被称为“轻量级”对象,因为创建它的代价小。Java 中的 Iterator 功能比较简单,并且只能单向移 动:
1. 使⽤方法 iterator() 要求容器器返回一个 Iterator。第一次调用 Iterator 的 next() 方法时,它返回序列的第一个 元素。注意:iterator() 方法是 java.lang.Iterable 接口,被 Collection 继承。
2. 使用 next() 获得序列中的下一个元素。
3. 使用 hasNext() 检查序列中是否还有元素。
4. 使用 remove() 将迭代器新返回的元素删除。
21、 Iterator 和 ListIterator 有什么区别?
Iterator 可用来遍历 Set 和 List 集合,但是 ListIterator 只能用来遍历 List。Iterator 对集合只能是前向遍历, ListIterator 既可以前向也可以后向。ListIterator 实现了 Iterator 接口,并包含其他的功能,比如:增加元素,替 换元素,获取前一个和后一个元素的索引等等。
22、Iterator 和 Enumeration 接口的区别?
与 Enumeration 相比,Iterator 更加安全,因为当一个集合正在被遍历的时候,它会阻止其它线程去修改集合。 否则会抛出 ConcurrentModificationException 异常。这其实就是 fail-fast 机制。具体区别有三点:
- Iterator 的方法名比 Enumeration 更科学;
- Iterator 有 fail-fast 机制,比 Enumeration 更安全;
- Iterator 能够删除元素,Enumeration 并不能删除元素。
23、fail-fast 与 fail-safe 有什么区别?
Iterator 的 fail-fast 属性与当前的集合共同起作用,因此它不会受到集合中任何改动的影响。Java.util 包中的所有 集合类都被设计为 fail-fast 的,而 java.util.concurrent 中的集合类都为 fail-safe 的。当检测到正在遍历的集合的 结构被改变时,fail-fast 迭代器抛出ConcurrentModificationException,而 fail-safe 迭代器从不抛出 ConcurrentModificationException。
24、Collection 和 Collections 有什么区别?
Collection:是最基本的集合接口,一个 Collection 代表一组 Object,即 Collection 的元素。它的直接继承接口有
List,Set 和 Queue。
Collections:是不属于 Java 的集合框架的,它是集合类的一个工具类/帮助类。此类不能被实例例化, 服务于 Java的 Collection 框架。它包含有关集合操作的静态多态方法,实现对各种集合的搜索、排序、线程安全等操作。
java并发
1、并行和并发有什么区别?
1. 并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间隔发⽣
2. 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件;
3. 在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如 Hadoop 分布式集群。所以并发 编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。
2、线程和进程的区别?
进程:是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程 中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。
线程:是进程的一个实体,是 cpu 调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的 多个线程之间可以并发执行。
3、守护线程是什么?
守护线程(即 Daemon thread),是个服务线程,准确地来说就是服务其他的线程
4、创建线程的几种方式?
1. 继承 Thread 类创建线程;
2. 实现 Runnable 接口创建线程;
3. 通过 Callable 和 Future 创建线程;
4. 通过线程池创建线程。
5、Runnable 和 Callable 有什什么区别?
1. Runnable 接口中的 run() 方法的返回值是 void,它做的事情只是纯粹地去执行 run() 方法中的代码而已;
2. Callable 接口中的 call() 方法是有返回值的,是一个泛型,和 Future、FutureTask 配合可以⽤用来获取异步执行的结果。
6、线程状态及转换?
Thread 的源码中定义了了6种状态:new(新建)、runnnable(可运行)、blocked(阻塞)、waiting(等待)、time waiting (定时等待)和 terminated(终止)。
线程状态转换如下图所示:
7、sleep() 和 wait() 的区别?
1、sleep() 方法正在执行的线程主动让出 cpu(然后 cpu 就可以去执行其他任务),在 sleep 指定时间后 cpu 再回 到该线程继续往下执行(注意:sleep 方法只让出了 cpu,而并不会释放同步资源锁);而 wait() 方法则是指当前 线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了 notify() 方 法,之前调用 wait() 的线程才会解除 wait 状态,可以去参与竞争同步资源锁,进而得到执行(注意:notify 的 作用相当于叫醒睡着的人,而并不会给他分配任务,就是说 notify 只是让之前调用 wait 的线程有权利重新参与线 程的调度);
2、 sleep() 方法可以在任何地方使用,而 wait() 方法则只能在同步方法或同步块中使用;
3、 sleep() 是线程类(Thread)的方法,调用会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到 时间自动恢复;wait() 是 Object 的方法,调用会放弃对象锁,进入等待队列,待调用 notify()/notifyAll() 唤醒指定 的线程或者所有线程,才会进入锁池,不再次获得对象锁才会进入运行状态。
8、线程的 run() 和 start() 有什么区别?
1、每个线程都是通过某个特定 Thread 对象所对应的方法 run() 来完成其操作的,方法 run() 称为线程体。通过调用 Thread 类的 start() 方法来启动一个线程;
2、start() 方法来启动一个线程,真正实现了多线程运行。这时无需等待 run() 方法体代码执行完毕,可以直接继 续执行下面的代码;这时此线程是处于就绪状态,并没有运行。然后通过此 Thread 类调用方法 run() 来完成其运行状态,这里方法 run() 称为线程体,它包含了要执行的这个线程的内容,run() 方法运行结束,此线程终止。然后cpu 再调度其它线程;
3、 run() 方法是在本线程里的,只是线程里的一个函数,而不是多线程的。如果直接调用 run(),其实就相当于是 调用了一个普通函数而已,直接待⽤ run() 方法必须等待 run() 方法执⾏行完毕才能执行下面的代码,所以执行路径 还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用 start() 方法而不是 run() 方法。
9、在 Java 程序中怎么保证多线程的运行安全?
线程安全在三个方面体现:
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized、volatile);
有序性:⼀一个线程观察其他线程中的指令执⾏行行顺序,由于指令重排序,该观察结果一般杂乱⽆无序,(happens-before 原则)。
10、Java 线程同步的几种方法?
- 使⽤用 Synchronized 关键字;
- wait 和 notify;
3. 使⽤用特殊域变量 volatile 实现线程同步;
4. 使⽤用可重入锁实现线程同步;
5. 使用阻塞队列实现线程同步;
6. 使用信号量 Semaphore。
12、谈谈对 ThreadLocal 的理解?
1、Java 的 Web 项目大部分都是基于 Tomcat。每次访问都是一个新的线程,每一个线程都独享一个 ThreadLocal,我们可以在接收请求的时候 set 特定内容,在需要的时候 get 这个值。
2、 ThreadLocal 提供 get 和 set 方法,为每一个使用这个变量的线程都保存有一份独立的副本。
1、get() 方法是用来获取 ThreadLocal 在当前线程中保存的变量副本;
2、set() 用来设置当前线程中变量的副本;
3、 remove() 用来移除当前线程中变量的副本;
4、 initialValue() 是一个 protected 方法,一般是用来在使用时进行重写的,如果在没有 set 的时候就调用 get, 会调用 initialValue 方法初始化内容。
13、在哪些场景下会使用到 ThreadLocal?
在调用 API 接口的时候传递了一些公共参数,这些公共参数携带了一些设备信息(是安卓还是 ios),服务端接口 根据不同的信息组装不同的格式数据返回给客户端。假定服务器端需要通过设备类型(device)来下发下载地址, 当然接口也有同样的其他逻辑,我们只要在返回数据的时候判断好是什么类型的客户端就好了了。上面这种场景就可以将传进来的参数 device 设置到 ThreadLocal 中。用的时候取出来就行。避免了参数的层传递。
14、说一说自己对于 synchronized 关键字的了解?
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或 者代码块在任意时刻只能有一个线程执行。
16、如何在项目中使用 synchronized 的?
synchronized 关键字最主要的三种使用方式:
1、 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁;
2、修饰静态方法:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何⼀个实例对象,是类成员(static 表明这是该类的一个静态资 源,不管 new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程 A 调用一个实例 对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许 的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized方法占⽤用的锁是当前实例例对象锁;
3、 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。和 synchronized 方 法一样,synchronized(this) 代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是给 Class 类上锁。这里再提一下:synchronized 关键字加到非 static 静态方 法上是给对象实例上锁。另外需要注意的是:尽量不不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓冲功能。
补充:双重校验锁实现单例例模式
问到 synchronized 的使⽤用,很有可能让你用 synchronized 实现个单例模式。这里补充下使用 synchronized 双重 校验锁的方法实现单例模式:
另外,需要注意 uniqueInstance 采⽤用 volatile 关键字修饰也是很有必要。采用 volatile 关键字修饰也是很有必要 的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1 -> 3 -> 2。指令重排在单线程环境下不不会出现问题,但 是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调⽤用 getUniqueInstance() 后发现 uniqueInstance 不不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未 被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
34、说下对线程池的理解?为什么要使用线程池?
线程池提供了一种限制和管理资源(包括执行一个任务)的方式。每个线程池还维护一些基本统计信息,例如:已 完成任务的数量。
使用线程池的好处
1、降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
2、 提高响应速度:当任务到达时,任务可以不需要的等到线程创建就能立即执行;
3、 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性, 使用线程池可以进行统一的分配,调优和监控。
36、如何创建线程池?
方式一:通过 ThreadPoolExecutor 的构造方法实现:
方式二:通过 Executor 框架的工具类 Executors 来实现: 我们可以创建三种类型的 ThreadPoolExecutor:
1、FixedThreadPool:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的 任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空 闲时,便处理在任务队列中的任务。
2、 SingleThreadExecutor:方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被 保存在一个任务队列中,待线程空闲,按先进先出的顺序执行队列中的任务。
3、CachedThreadPool:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但 若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,⼜又有新的任务提交,则会创建新的线 程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
注意:
阿里巴巴Java开发⼿手册》中强制线程池不允许使用 Executors 去创建,⽽是通过 ThreadPoolExecutor 的方式,这 样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 创建线程池对象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor :允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请 求,从⽽而导致 OOM。CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
40、JDK 中提供了哪些并发容器?
JDK 提供的这些容器⼤部分在 java.util.concurrent 包中。
- ConcurrentHashMap:线程安全的 HashMap;
- CopyOnWriteArrayList:线程安全的 List,在读多写少的场合性能⾮非常好,远远好于 Vector;
3. ConcurrentLinkedQueue:⾼高效的并发队列列,使⽤用链表实现。可以看做⼀一个线程安全的 LinkedList,这是⼀一 个⾮非阻塞队列列;
4. BlockingQueue:这是一个接口,JDK 内部通过链表、数组等⽅方式实现了了这个接⼝口。表示阻塞队列列,非常适合用于作为数据共享的通道;
5. ConcurrentSkipListMap:跳表的实现。这是⼀一个 Map,使⽤用跳表的数据结构进行行快速查找。