Java面向对象
面向对象和面向过程的区别?
面向对象是一种基于面向过程的编程思想,是向现实世界模型的自然延伸,这是一种“万物皆对象”的编程思想。由执行者变为指挥者,在现实生活中的任何物体都可以归为一类事物,而每一个个体都是一类事物的实例。面向对象的编程是以对象为中心,以消息为驱动。
区别:
- 编程思路不同:面向过程以实现功能的函数开发为主,而面向对象要首先抽象出类、属性及其方法,然后通过实例化类、执行方法来完成功能。
- 封装性:都具有封装性,但是面向过程是封装的是功能,而面向对象封装的是数据和功能。
- 面向对象具有继承性和多态性,而面向过程没有继承性和多态性,所以面向对象优势很明显
面向对象的三大特性?
**面向对象:**面向对象是一种思想,将功能封装进对象之中,让对象去实现具体的细节;这种思想是将数据作为第一位,而方法或者说是算法作为其次,这是对数据一种优化,操作起来更加的方便,简化了过程。
- 封装:通常认为封装是把数据和操作数据的方法封装起来,对数据的访问只能通过已定义的接口。
- 继承:继承是从已有类得到继承信息创建新类的过程。子类可以扩展父类的功能,但不能改变父类原有的功能(不要重写父类的方法),继承应该遵循里氏替换原则。
- 多态:分为编译时多态(方法重载)和运行时多态(方法重写)。要实现多态需要做两件事:一是子类继承父类并重写父类中的方法;二是用父类型引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为;
https://juejin.cn/post/6907627284790788104
接口和抽象类的异同?【美团2】
- 抽象类用abstract修饰,接口用interface修饰;
- 抽象类中可以有抽象方法和具体方法,接口中只能有抽象方法;
- 抽象类中可以定义构造函数,接口不能定义构造函数;
- 抽象类中的成员权限可以是 public、默认、protected(抽象类中抽象方法就是为了重写,所以不能被 private 修饰);接口中的成员只可以是 public(方法默认:public abstrat、成员变量默认:public static final)
- 一个类只能继承一个抽象类;一个类可以实现多个接口;
抽象类和普通类的区别?
- 普通类可以去实例化调用;抽象类不能被实例化,因为它是存在于一种概念而不非具体。
- 普通类和抽象类都可以被继承,但是抽象类被继承后子类必须重写继承的方法,除非自类也是抽象类。
抽象类中构造器的作用?【美团2】
Java抽象类的构造方法和普通类的构造方法一样都是用来初始化类。只是抽象类的构造方法不能直接调用 **因为抽象类不能实现实例。**但是一旦一个普通类继承了抽象类,便可以在构造函数中调用其抽象类的构造函数。
abstract class Animal {
Animal(){
System.out.println("抽象类Animal无参构造器"); //此处执行前会默认执行super()
}
Animal(int a){
System.out.println("抽象类Animal有参构造器");
}
}
public class Horse extends Animal {
Horse () {
System.out.println("子类horse无参构造器"); //此处执行前会默认执行super()
}
Horse (int h) {
super(3);
System.out.println("子类horse有参构造器");
}
public static void main(String [] args) {
Horse h = new Horse();
System.out.println("---------------------");
Animal h2 = new Horse(6);
// Animal h3 = new Animal(); //无法编译,抽象类不可实例化
}
}
抽象类Animal无参构造器
子类horse无参构造器
---------------------
抽象类Animal有参构造器
子类horse有参构造器
接口是什么?为什么要使用接口而不是直接使用具体类?
- 接口用于定义API,它定义了类必须得遵循的规则。它提供了一种抽象。
- 可以有多重实现:如 List 接口,你可以使用可随机访问的 ArrayList,也可以使用方便插入和删除的 LinkedList。
- 接口中不允许普通方法,以此来保证抽象,但是 Java 8 中你可以在接口声明静态方法和默认普通方法。
为什么接口中的成员变量必须是public static final的?
首先明白一个原理,就是接口的存在意义。接口就是为了实现多继承的抽象类,是一种高度抽象的模板、标准或者说协议。规定了什么东西该是这样,如果你继承了我这接口,就必须这样。比如USB接口,就是小方口,两根电源线和两根数据线,不能多不能少。
public: 使接口的实现类可以使用这个常量;
static:static修饰就表示它属于类的,随的类的加载而存在的,如果是非static的话,就表示属于对象的,只有建立对象时才有它,而接口是不能建立对象的,所以接口的常量必须定义为static;
final:final修饰就是保证接口定义的常量不能被实现类去修改,如果没有final的话,由子类随意去修改的话,接口建立这个常量就没有意义了。
abstract关键字的作用?
- 被abstract修饰的方法是抽象方法,只有方法声明没有方法体;
- 类中只要有一个抽象方法,这个类就是抽象类;
- 抽象类中的方法可以有抽象方法,也可以有非抽象方法;
- 一个类只有把抽象类中的方法全部实现,那么这个类才算是普通类,否则它还是抽象类;
- 抽象类不能被实例化;
extends和implement 区别?
- extends是继承父类,只要那个类不是声明为final或者那个类定义为abstract的就能继承,
- 继承只能继承一个类,但implements可以实现多个接口,用逗号分开就行。
比如class A extends B implements C,D,E
interface的引入是为了部分地提供多继承的功能。在interface中只需声明方法头,而将方法体留给实现的class来做。这些实现的class的实例完全可以当作interface的实例来对待。在interface之间也可以声明为extends(多继承)的关系。
重写和重载的区别?
** ** | 重写 | 重载(两同三不同) |
---|---|---|
访问权限 | 访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private) | 不影响 |
返回类型 | 返回值类型和父类相同类型或者是子类的类型 | 不影响 |
方法名 | 重写的方法名相同 | 重载的方法名相同 |
参数列表 | 参数列表必须完全与被重写的方法相同 | 必须具有不同的参数列表(参数个数、参数顺序、参数返回类型不同) |
抛出异常 | 抛出异常的范围不大于被重新的方法的异常范围。 | 不影响 |
作用范围 | 父类和子类之间 | 同一个类中 |
Java 中是否可以重写一个 private 或者 static 方法?
- Java 中 static 方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而 static 方法是编译时静态绑定的。static 方法跟类的任何实例都不相关,所以概念上不适用。
- Java 中也不可以覆盖 private 的方法,因为 private 修饰的变量和方法只能在当前类中使用, 如果是其他的类继承当前类是不能访问到 private 变量或方法的,当然也不能覆盖。
静态的方法可以被继承,但是不能重写。如果父类和子类中存在同样名称和参数的静态方法,那么该子类的方法会把原来继承过来的父类的方法隐藏,而不是重写。
通俗的讲就是父类的方法和子类的方法是两个没有关系的方法,具体调用哪一个方法是看是哪个对象的引用;这种父子类方法也不在存在多态的性质。
Java访问修饰符的作用范围?
** ** | 同一个类 | 同一个包其他类 | 其他包子类 | 其他包其他类 |
---|---|---|---|---|
private | 可以 | 不可以 | 不可以 | 不可以 |
缺省 | 可以 | 可以 | 不可以 | 不可以 |
protected | 可以 | 可以 | 可以 | 不可以 |
public | 可以 | 可以 | 可以 | 可以 |
Java为什么没有多继承?
**会产生钻石危机。**我们假设类A、B、C内的方法都是 public 的,以方便讨论。
假设类A中有一个public方法fun(),然后类B和类C同时继承了类A,类B或类C中各自对fun()进行了覆盖,这时类D通过多继承同时继承了类B和类C,这样便导致砖石危机了,程序在运行的时候对应方法fun()该如何判断?看到这里,相信大家也明白了多重继承会导致这种有歧义的情况存在。
- 若子类继承的父类中拥有相同的成员变量,子类在引用该变量时将无法判别使用哪个父类的成员变量。
- 若一个子类继承的多个父类拥有相同方法,同时子类并未覆盖该方法(若覆盖,则直接使用子类中该方法),那么调用该方法时将无法确定调用哪个父类的方法。
什么是构造器?需要注意点有哪些?
- 构造器的名字和类名相同,构造器没有返回值,但是也不能用void声明。
- 构造方法不可被重写,可以重载。
- 当生成类对象的时候,会自动执行,无需调用。
- 不能被static、final、native、abstract和synchronized修饰。
- 如果显式给定带参的构造器后,系统就不再提供默认的无参构造器。
- 子类可以通过super语句调用父类的构造方法。
- 在接口中不可以有构造方法。
- 在抽象类中必须有构造方法,只是不能直接创建抽象类的实例对象。实例化子类的时候,会先初始化父类,不管父类是不是抽象类都会调用父类的构造方法。
空参构造器的作用?
- 子类继承父类的时候会默认调用super(),如果父类没有显示编写有参构造器,那么会默认存在空参构造器,否则会报错。
- 便于反射时,创建运行时类的对象。
局部变量为什么要手动初始化?
局部变量是指类方法中的变量,必须初始化。
局部变量运行时被分配在栈中(量大、生命周期短),如果虚拟机给每个局部变量都初始化一下,是一笔很大的开销,但变量不初始化为默认值就使用是不安全的。
出于速度和安全性两个方面的综合考虑,解决方案就是虚拟机不初始化,但要求编写者一定要在使用前给变量赋值。
成员变量与局部变量的区别?
|
| 成员变量 | 局部变量 |
| — | — | — |
| 在类中的位置不同 | 在类中方法外面 | 在方法内或者参数列表中 |
| 数据存储位置不同 | 在堆中(方法区中的静态区) | 在栈中 |
| 生命周期不同 | 随着对象的创建而存在,随着对象的消失而消失 | 随着方法的调用而存在,随着方法的调用完毕而消失 |
| 是否有默认初始值 | 有默认初始值 | 没有默认初始值,使用之前需要赋值,否则编译器会报错 |
static关键字的作用?
static可以修饰变量、方法、代码块、内部类。
- 静态变量:这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份;静态变量在类加载的初始化阶段赋值。
- **静态方法:**属于类所有,随着类的加载而存在,可以通过【对象名.方法名】和【类名.方法名】两种方式来访问,静态方法只能调用静态方法和静态变量,不能访问非静态方法和非静态变量。
- **静态代码块:**静态代码块在类初始化时运行一次(只运行一次),主要作用是实现 static 属性的初始化。
- 静态内部类:非静态内部类依赖于外部类的实例,而静态内部类不需要。静态内部类不能访问外部类的非静态的变量和方法。
父类、子类的静态代码块和构造方法的执行顺序?
执行顺序记住两个优先:静态优先,父类优先!
初始化顺序:静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。
什么时候会用到静态内部类?
静态内部类的主要特点:
- 不持有外部类的引用(普通内部类持有);
- 可以直接创建实例,不需要先创建外部类(普通内部类需要);
- 可以有静态成员变量、方法(普通内部类不行)和非静态成员变量、方法;
- 只可以直接访问外部类静态成员,不可以直接访问外部类的非静态成员(普通内部类可以),需要通过传入外部类引用的方式才能访问。
使用场景:
- 外部类与内部类有很强的联系,需要通过内部类的方式维持嵌套的可读性。
- 内部类可以单独创建。
- 内部类不依赖于外部类,外部类需要使用内部类,而内部类不需使用外部类(或者不合适持有外部类的强引用)。
静态变量与实例变量的区别?
- 静态变量是用static修饰的变量,属于类,被类的所有对象所共有,静态变量在内存中有且仅有一个拷贝。可以直接使用“类名.对象名”访问。静态变量和全局变量都存在方法区
- 实例变量是属于某一实例的,需要先创建对象,然后通过对象才能够访问的到。存储在堆内存中。
静态方法和非静态方法的区别?
|
| 静态方法 | 非静态方法 |
| — | — | — |
| 生命周期不同 | 随着类的加载而存在 | 随着对象的创建而存在 |
| 调用方式不同 | 类名.方法名 or 对象名.方法名
【调用静态方法无需创建对象】 | 对象名.方法名 |
| 访问的限制 | 只能访问静态变量 or 静态方法 | 可以访问静态和非静态属性和方法 |
| 是否可以使用super和this | 不能使用this和super(因为静态方法存在的时候不一定存在对象) | 可以使用this和super |
如何确定一个属性和方法是否要声明为static?
- 属性是可以被多个对象所共享的,不会随着对象的不同而不同的。这样的属性需要声明为static。
- 工具类中的方法,习惯上声明为静态方法,这样可以直接通过类名.方法名来调用,不需要实例化一个对象来调用方法。 比如:Math、Arrays、Collections。
在静态方法中为什么不可以调用非静态成员?
因为不需要实例对象就可以通过类名来调用静态方法,所以静态方法里面不可以调用非静态属性或非静态方法。(生命周期不同)
抽象类中可以有静态方法吗?
抽象类中不能有静态的抽象方法。
- 静态方法是不需要实例化对象,在类实例化之前就分配内存的,通过类名.方法名就可以访问的。但是抽象类是不能够实例化的,即不能被分配内存。
- 另外,定义抽象方法的目的是重写此方法,但如果定义成静态方法就不能被重写。
Java的虚函数如何实现?
虚函数的存在是为了多态。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为**“虚”函数**。
C++中普通成员函数加上virtual关键字就成为虚函数。Java中的虚函数就是普通函数(没有被static、final修饰的函数),动态绑定是Java的默认行为。
package test;
/**
* Created by ping on 2015/10/12.
*/
class A{
public void FUN(){
System.out.println("FUN in A is called");
}
}
class B extends A{
public void FUN(){
System.out.println("FUN in B is called");
}
}
public class VirtualTest {
public static void main(String args[]) {
A a = new A();
B b = new B();
A p;
p = a;
p.FUN();
p = b;
p.FUN();
}
}
在上面的代码中,我们定义了一个基类指针(在java中应该叫引用)去指向不同的对象,可以发现同样可以实现多态。 也就是说,java的普通成员函数(没有被static、native等关键字修饰)就是虚函数,原因很简单,它本身就实现虚函数实现的功能------多态。
C++ Java
虚函数 -------- 普通函数
纯虚函数 -------- 抽象函数
抽象类 -------- 抽象类
虚基类 -------- 接口
super关键字的作用?
- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。
- 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。
- this 和 super 不能同时出现在一个构造函数里面,因为 this 必然会调用其它的构造函数,其它的构造函数必然也会有 super 语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
JDK、JRE和JVM三者之间的关系?
- **JDK:**是Java开发工具包,是整个 Java 的核心,包括了 Java 运行环境 JRE、Java 工具和 Java 基础类库。
- **JRE:**是 Java 的运行环境,包含 JVM 标准实现及 Java 核心类库。
- JVM:是 Java 虚拟机,是整个Java 实现跨平台的最核心的部分,能够运行以 Java 语言写作的软件程序。所有的 Java 程序会首先被编译为 .class 的类文件,这种类文件可以在虚拟机上执行。
- **JDK和JRE内部都有JVM:**如果只是为了运行程序,那么只需装JRE即可,如果需要编程那么需要装JDK。但是有时候不在计算机进行编程也需要装JDK,因为有些其他软件需要JDK环境的支持(比如:Neo4j,在使用Neo4j图数据库前,需要预装JDK环境, 因为Neo4j是用Java编写的。
Java和C++的异同?
** ** | Java | C++ |
---|---|---|
面向对象的特点 | 都支持继承、封装和多态 | |
有无指针 | 不提供指针来访问内存 | 提供指针来访问内存 |
是否自动回收垃圾 | 有自动垃圾回收机制GC | 没有自动垃圾回收机制,需要程序员手动释放 |
是否支持多继承 | 不支持多继承,通过实现接口可以间接实现多继承 | 支持多继承 |
Integer和int的区别?
- Integer是int的包装类,int则是java的一种基本数据类型;
- Integer变量必须实例化后才能使用,而int变量不需要;
- Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值;
- Integer的默认值是null,int的默认值是0;
- int只能使用“”来判断数据的大小,Integer使用“”来比较包装类型的地址是否相同,equals()方法如果没有重写,比较的是地址,如果重写了那么比较的是内容。
- Integer可以用于泛型,int不能用于泛型。
谈一下对于String的理解?
String底层数据结构和实现原理?
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
- String 被声明为 final,因此它不可被继承。
- 内部使用 char 数组存储数据,该数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。
- String类实现了Serializable接口,说明String类可以进行序列化和反序列化。
String为什么是final?不可变的好处?
1. 可以缓存 hash 值
因为 String 的 hash 值经常被使用。例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
2. String Pool 的需要
如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
![image.png](https://img-blog.csdnimg.cn/img_convert/96ecc29c9dd5486adb9e57fdb47de800.png#clientId=u9f9bdec0-f6ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=G4Cp1&margin=[object Object]&name=image.png&originHeight=216&originWidth=502&originalType=url&ratio=1&rotation=0&showTitle=false&size=35243&status=done&style=stroke&taskId=u5ee19a7c-ae47-44d2-9ab4-f662405b7c8&title=)
3. 安全性
String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。
4. 线程安全
String 不可变天生具备线程安全,可以在多个线程中安全地使用。
有什么办法能改变String的不可变性?
使用反射。
final修饰StringBuffer后还可以append吗?
可以。final 修饰的是一个引用变量,那么这个引用始终只能指向这个对象,但是这个对象内部的属性是可以变化的。
String str = “abc” 和String str = new String(“abc”)的区别?
String str ="abc"原理
采用字面值的方式创建时,JVM会先去字符串常量池中去查找是否存在"abc"这个对象:
- 如果不存在就创建这个字符串,并把地址返回给str;
- 如果存在则直接把"abc"这个字符串的地址返回给str;
String x = “abc”;
String y = “abc”;
System.out.println(x==y);//结果为true
String str = new String(“abc”)原理
采用new关键字的方式创建,JVM也会去字符串常量池中查找有没有这个字符串:
- 如果没有,先在字符串常量池里创建"abc"这个字符串,然后再复制一份放在堆里并把地址返回给str。
- 如果有,那么就直接复制一份放在堆里并把地址返回给str。
String x = new String(“abc”);
String y = new String(“abc”);
System.out.println(x == y);//结果为false
String、StringBuffer 和 StringBuilder的区别? 【美团2】
** ** | String | StringBuffer | StringBuilder |
---|---|---|---|
是否可变性 | 字符串常量,底层用final关键字修饰char []value,具有不可变性 | 继承自AbstractStringBuiler类(也是用char[] value 来保存字符串,但是没有用final来修饰),所以StringBuffer和StringBuilder具有可变性。 | |
线程安全性 | String是字符串常量,线程安全 | 对方法或对调用的方法加sychronized锁,线程安全 | 线程不安全 |
使用场景 | 操作少,数据少 | 多线程,操作多,数据多 | 单线程,操作多,数据多 |
性能(速度) | StringBuilder>StringBuffer>String |
String.intern()
使用 String.intern() 可以保证相同内容的字符串变量引用同一的内存对象。
下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同对象,而 s3 是通过 s1.intern() 方法取得一个对象引用。intern() 首先把 s1 引用的对象放到 String Pool(字符串常量池)中,然后返回这个对象引用。因此 s3 和 s1 引用的是同一个字符串常量池的对象。
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2); // false
String s3 = s1.intern();
System.out.println(s1.intern() == s3); // true
如果是采用 “bbb” 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Pool 中。
String s4 = "bbb";
String s5 = "bbb";
System.out.println(s4 == s5); // true
String类的常用方法有哪些?
- indexOf():返回指定字符的索引。
- charAt():返回指定索引处的字符。
- replace():字符串替换。
- trim():去除字符串两端空白。
- split():分割字符串,返回一个分割后的字符串数组。
- getBytes():返回字符串的 byte 类型数组。
- length():返回字符串长度。
- toLowerCase():将字符串转成小写字母。
- toUpperCase():将字符串转成大写字符。
- substring():截取字符串。
- equals():字符串比较。
final关键字总结?【美团2】
- 被final修饰的类不能被继承。
- 被final修饰的方法不能被重写。使用private修饰的方法被隐式地定义为final方法。
- 被final修饰的数据。
- 对于基本类型,final 使数值不变;
- 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
- 类中所有private方法都被隐式地指定为final。
final、finally、finalize()的区别?
- final可以用来修饰变量、方法、类。final修饰变量,该变量是常量不可改变;final修改方法,该方法不可被重写;final修饰类,该类不可被继承。
- finally是一个Java关键字,和try块一起使用,try….finally的代码块,finally代码块里面的内容一定会执行。像数据库连接、输入输出流、网络编程Socket等资源,JVM是不能自动的回收的,我们需要自己手动的进行资源的释放。此时的资源释放,就需要声明在finally中。
- finalize()是Object类中的方法,使用finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
谈一谈关于equals和hashCode的理解?
Object类中都有哪些方法?
public final native Class<?> getClass()//用于获取对象的类
public native int hashCode()//用于获取对象的哈希值
public boolean equals(Object obj)//用于比较两个对象的地址是否相同
protected native Object clone() throws CloneNotSupportedException //用于对象之间的拷贝
public String toString()//用于把对象转换成字符串
public final native void notify() //用于通知对象释放资源
public final native void notifyAll() //用于通知所有对象释放资源
public final native void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final void wait() throws InterruptedException
protected void finalize() throws Throwable {}//用于告知垃圾回收器进行垃圾回收
equals()方法
- **对称性:**如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true”
- **自反性:**x.equals(x)必须返回是“true”
- **传递性:**如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也应该返回是“true”
- **一致性:**如果x.equals(y)返回是“true”,只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是“true”
- **非空性:**对于任意非空引用x,x.equals(null),永远返回是“false”
和equals()的区别?基本数据类型与String类型使用和equals()的注意事项?
- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
- 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。
Integer x = new Integer(1);
Integer y = new Integer(1);
System.out.println(x.equals(y)); // true
System.out.println(x == y); // false
为什么重写equals方法必须重写hashCode方法?【美团2】
- 如果只重写equals,不重写hashCode会怎样?
- 这样只能使用equals比较两个对象是否相等,如果重写hashCode,可以先比较hashCode,如果hashCode不相等,说明对象肯定不相等。
- 还可能造成相等的对象散列到不用的位置,造成对象的不能覆盖问题,导致存在两个相等的对象。
- 只重写 hashCode 会怎样呢?
没法区分两个hashCode相同的对象是否是用一个。
String类的hashCode与equals是怎么重写的?
String的equals():[地址是否相同->是否为同一对象的实例->长度或大小->每个元素的值是否相同]
- 首先判断两个对象的地址是否相同,如果相同直接返回true。
- 否则继续判断,判断两个对象是否来自同一个对象的实例,如果不是直接返回false。
- 否则继续判断,判断两个String对象的长度是否相同,如果不相同直接返回false。
- 如果相同继续判断,判断每个位置的值是否相同,如果有一个不相同直接返回false。
- 执行完所有语句,如果没有返回false,返回true。
//重写equals方法
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
//重写hashCode方法
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;
}
在Java的集合中是怎么判断两个对象是否相等的?
- 首先判断两个对象的地址是否相同,如果相同直接返回true。
- 否则继续判断,判断两个对象是否来自同一个对象的实例,如果不是直接返回false。
- 否则继续判断,判断两个集合对象的size是否相同,如果不相同直接返回false。
- 如果相同继续判断,判断每个元素是否相同,如果遇到不相同直接返回false。
- 执行完所有语句,如果没有返回false,那么返回true。
两个对象的 hashCode() 相同,则 equals() 也一定为 true 吗?
两个对象的 hashCode() 相同,equals() 不一定为 true。因为在散列表中,hashCode() 相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等【散列冲突】。
值传递和引用传递的区别?
值传递:在调用函数的时候,将实参复制一份直接传递到函数中,如果函数对形参进行修改的时候,不会影响到实参。
引用传递:在调用函数的时候,将实参的地址传递到函数中,如果函数对形参进行修改的时候,会影响到实参的值。
Java中为什么只有值传递?
class ParamTest{
public static void main(String[] args) {
ParamTest pt = new ParamTest();
User hollis = new User();
hollis.setName("Hollis");
hollis.setGender("Male");
pt.pass(hollis);
System.out.println("print in main , user is " + hollis);
}
public void pass(User user) {
user = new User();
user.setName("hollischuang");
user.setGender("Male");
System.out.println("print in pass , user is " + user);
}
}
//输出结果:
print in pass , user is User{name='hollischuang', gender='Male'}
print in main , user is User{name='Hollis', gender='Male'}
当我们在main中创建一个User对象的时候,在堆中开辟一块内存,其中保存了name和gender等数据。然后hollis持有该内存的地址0x123456
(图1)。
当尝试调用pass方法,并且hollis作为实际参数传递给形式参数user的时候,会把这个地址0x123456
交给user,这时,user也指向了这个地址(图2)。
然后在pass方法内对参数进行修改的时候,即user = new User();
,会重新开辟一块0X456789
的内存,赋值给user。后面对user的任何修改都不会改变内存0X123456
的内容(图3)。
上面这种传递是什么传递?肯定不是引用传递,如果是引用传递的话,在执行user = new User();的时候,实际参数的引用也应该改为指向0X456789
,但是实际上并没有。
通过概念我们也能知道,这里是把实际参数的引用地址复制了一份,传递给了形式参数。所以,上面的参数其实是值传递,把实参对象引用的地址当做值传递给了形式参数。
如何实现对象的克隆?
(1)实现 Cloneable 接口并重写 Object 类中的 clone() 方法;
(2)实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深克隆。
深克隆和浅克隆的区别?
- 浅克隆:拷贝对象和原始对象的引用类型引用同一个对象。浅克隆只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化,这就是浅克隆。
- 深克隆:拷贝对象和原始对象的引用类型引用不同对象。深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变,这就是深拷贝(例:JSON.parse() 和 JSON.stringify(),但是此方法无法复制函数类型)。
Java 中定义的 clone 没有深浅之分,都是统一的调用 Object 的 clone 方法。为什么会有深克隆的概念?是由于我们在实现的过程中刻意的嵌套了 clone 方法的调用。也就是说深克隆就是在需要克隆的对象类型的类中重新实现克隆方法 clone()。
什么是隐式转换和显示转换?
- 隐式转换是自动类型转换:小类型的数据类型自动接受大类型的数据类型。
- 显示转换是强制类型转换:把一个大类型的数据强制转换成小类型的数据。
Java创建对象的几种方式?
方法 | 实现 | 是否调用构造方法 |
---|---|---|
使用new关键字 | Person p = new Person(); | 调用构造方法 |
使用Class类的newInstance方法 | Person p = (Person)Class.forName("com.liuke.Person ").newInstance(); | 调用构造方法 |
使用Constructor类的newInstance方法 | Constructor constructor = Person.class.getConstructor(); | |
Person p = constructor.newInstance(); | 调用构造方法 | |
使用clone()方法 | Person p2 = (Person) p.clone(); | 没有使用构造方法 |
使用反序列化 | ObjectInputStream in = new ObjectInputStream(new FileInputStream(“data.obj”)); | |
Person p= (Person) in.readObject(); | 没有使用构造方法 |
Java的8中基本数据类型?
数据类型 | 位数(字节数) | 默认值 | 取值范围 |
---|---|---|---|
byte | 8(1) | 0 | -27~27-1 |
short | 16(2) | 0 | -215~215-1 |
int | 32(4) | 0 | -231~231-1 |
long | 64(8) | 0 | -263~263-1 |
float | 32(4) | 0.0 | -231~231-1 |
double | 64(8) | 0.0 | -263~263-1 |
char | 16(2) | 空 | 0~2^16-1 |
boolean | 8(1) | false | true、false |
switch 语句能否作用在 byte 上,能否作用在 long 上,能否作用在 String 上?
在 switch(expr 1) 中,expr1 只能是一个整数表达式或者枚举常量。而整数表达式可以是 int 基本数据类型或者 Integer 包装类型。由于,byte、short、char 都可以隐式转换为 int,所以,这些类型以及这些类型的包装类型也都是可以的。
而** long 和 String 类型都不符合 switch 的语法规定**,并且不能被隐式的转换为 int 类型,所以,它们不能作用于 switch 语句中。
不过,需要注意的是在 JDK1.7 版本之后 switch 就可以作用在 String 上了。
自动装箱与自动拆箱?为什么要装箱?
- **自动装箱:**将八种基本数据类型(short、byte、int、float、long、double、boolean、char)用它们对应的引用类型包装起来,装箱过程是通过调用包装器的valueOf方法实现的。
- **自动拆箱:**将包装类型转换成基本数据类型,拆箱过程是通过调用包装器的 xxxValue方法实现的。
为什么要装箱?
Java早年设计的一个缺陷,基本数据类型不是对象,自然不是Object的子类,需要装箱才能把数据类型变成一个类,那就可以把装箱过后的基本数据类型当做一个对象,就可以调用object子类的接口。而且基本数据类型是不可以作为形参使用的,装箱后就可以。而且在jdk1.5之后就实现了自动装箱拆箱,包装数据类型具有许多基本数据类型不具有的功能,只是装箱拆箱过程会稍稍微的影响一下效率。
缓存池
new Integer() 和 Integer.valueOf()的区别:
1. new Integer(123) 每次都会新建一个对象
2. Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y); // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k); // true
编译器会在缓冲池范围内的基本类型自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。
Integer i = new Integer(10);
public class Main {
public static void main(String[] args) {
Integer i1 = 100; //自动装箱
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1==i2, i3==i4);// true, false
}
}
/*
原因:valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,
如果在的话就直接返回缓存池的内容。
*/
public static Integer valueOf(int i) {
if(i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128];
else
return new Integer(i);
}
在 Java 8 中,Integer 缓存池的大小默认为 -128~127。
基本类型对应的缓冲池如下:
- boolean values true and false
- all byte values
- short values between -128 and 127
- int values between -128 and 127
- char in the range \u0000 to \u007F
在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。
char型变量可不可以存储一个汉字?
可以存储,因为一个char型变量是两个字节,一个汉字正好两个字节,Java中使用的编码是Unicode。
字符常量和字符串常量的区别?
字符常量 | 字符串常量 |
---|---|
用单引号引起的一个字符 | 用双引号引起的若干个字符 |
是一个整形值,可以参与表达式运算 | 是一个地址值 |
占2个字节 | 占若干个字节 |
instanceof关键字的作用?
用来测试一个对象是否是一个类的实例(不能比较基本数据类型)。
Integer integer = new Integer(20);
System.out.println(integer instanceof Integer); //输出 true
谈一下对于异常的理解?
Java的异常体系?
Java错误(Error)和异常(Exception)的区别?
Java将所有的错误封装为一个对象,其根本父类为Throwable,Throwable有两个子类:Error和Exception。
- **Error错误:**程序中无法处理的错误,表示运行应用程序中出现严重错误。一般是虚拟机出现错误,无法处理需要终止程序,比如OOM和栈溢出。
- **Exception异常:**程序本身可以处理的异常,异常分为两类:运行时异常和编译时异常。
Exception中的编译时异常和运行时异常的区别?
- **运行时异常:RuntimeException类及其子类异常,**都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
- 编译时异常(非运行时异常):**Exception中除RuntimeException以外的异常,**编译时异常是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常。
Java异常中检查时异常和非检查异常区别?
- **检查时异常(Exception中的编译时异常):**是编译器要求必须处理的异常。当编译器检查到应用中的某处可能会有此类异常时,将会提示你处理本异常,要么使用try-catch捕获,要么使用throws 关键字抛出,否则编译不通过。
- **非检查时异常(Error和Exception中运行时异常):**编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有try-catch捕获它,也没有使用throws抛出该异常,编译也会正常通过。
异常总结
- try、catch和finally都不能单独使用,只能是try-catch、try-finally或者try-catch-finally。
- try语句块监控代码,出现异常就停止执行下面的代码,然后将异常移交给catch语句块来处理。
- finally语句块中的代码一定会被执行,常用于回收资源 。
- throws:声明一个异常,告知方法调用者。
- throw :抛出一个异常,至于该异常被捕获还是继续抛出都与它无关。
谈一谈关于注解的理解?
什么是注解?
注解在我的理解下,就是代码中的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相对应的处理。
你在开发中有没有用到注解?
- 注解其实在开发中是非常常见的,比如我们在使用各种框架时(像我们Java程序员接触最多的还是Spring框架一套),就会用到非常多的注解,@Controller / @Param / @Select 等等。一些项目也用到lombok的注解,@Slf4j / @Data 等等
- 除了框架实现的注解,Java原生也有@Overried、@Deprecated、@FunctionalInterface等基本注解。Java原生的基本注解大多数用于「标记」和「检查」
- 原生Java除了这些提供基本注解之外,还有一种叫做元Annotation(元注解),所谓的元Annotation就是用来修饰注解的。常用的元Annotation有**@Retention 和@Target**。
- @Retention注解可以简单理解为设置注解的生命周期
- @Target表示这个注解可以修饰哪些地方(比如方法、成员变量、包等等)
@Retention注解传入的是RetentionPolicy枚举,该枚举有三个常量,分别是SOURCE、CLASS和RUNTIME
- SOURCE代表着注解仅保留在源级别中,并由编译器忽略。
- CLASS代表着注解在编译时由编译器保留,但Java虚拟机(JVM)会忽略。
- RUNTIME代表着标记的注解会由JVM保留,因此运行时环境可以使用它。
一般来说,只要自定义的注解中@Retention注解设置为SOURCE和CLASS这俩个级别,那么就需要继承并实现AbstractProcessor(因为SOURCE和CLASS这俩个级别等加载到jvm的时候,注解就被抹除了)
lombok的实现原理就是在这(为什么使用了个@Data这样的注解就能有set/get等方法了,就是在这里加上去的)
我们自己定义的注解都是RUNTIME级别的,因为大多数情况我们是根据运行时环境去做一些处理。
自定义注解在项目中的应用?
RPC项目的应用
- 服务提供者需要在暴露的服务上增加注解 @RpcService,这个自定义注解是基于 @service 的,是一个复合注解,具备@service注解的功能,在@RpcService注解中指明服务接口和服务版本,发布服务到ZK上,会根据这个两个元数据注册
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service
public @interface RpcService {
Class<?> interfaceType() default Object.class;
String version() default "1.0";
}
服务提供者启动之后,根据spring boot自动装配机制,server-starter的配置类就生效了,在一个 bean 的后置处理器(RpcServerProvider)中获取被注解 @RpcService 修饰的bean,将注解的元数据注册到ZK上。
- 消费服务需要使用自定义的 @RpcAutowired 注解标识,是一个复合注解,基于 @Autowired。
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Autowired
public @interface RpcAutowired {
String version() default "1.0";
}
基于spring boot自动装配,服务消费者启动,bean后置处理器 RpcClientProcessor 开始工作,它主要是遍历所有的bean,判断每个bean中的属性是否有被 @RpcAutowired 注解修饰,有的话把该属性动态赋值代理类,这个再调用时会调用代理类的 invoke 方法。
谈一谈对于反射的理解?
什么是反射?
- 反射就是把java类中的各种成分映射成一个个的Java对象。例如:一个类有成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把个个组成部分映射成一个个对象。
- 反射能够在运行时获取类的信息;
- JVM加载完类后,会在堆内存的方法区中产生一个Class类型的对象(一个类只有一个Class对象),这个Class对象包含了完整的了结构信息。(这个对象就像一面镜子,通过这个镜子能够看到类的结构,这种方法就是反射。)
为什么要在运行时获取类的信息?
.java文件 -> .class文件 -> 加载到JVM运行,这就是所谓的运行时
在运行时获取类的信息,其实就是为了让我们所写的代码更具有「通用性」和「灵活性」
反射在框架中的应用?
- SpringMVC 你在方法上写上对象,传入的参数就会帮你封装到对象上
- Mybatis可以让我们只写接口,不写实现类,就可以执行SQL【动态代理->反射】
- 在类上加上@Component注解,Spring就帮你创建对象
- JDK动态代理其实就是运用了反射的机制,而CGLIB代理则用的是利用ASM框架,通过修改其字节码生成子类来处理
- 使用JDBC连接数据库时使用**Class.forName()**通过反射加载数据库的驱动程序。
反射机制的作用?
- 在运行时判断任意一个对象所属的类。
- 在运行时构造任意一个类的对象。
- 在运行时判断或调用任意一个类所具有的成员变量和方法。
- 在运行时获取泛型信息。
反射机制的优缺点?
优点:可以实现动态创建对象和编译,体现出很大的灵活性。通过反射机制我们可以获得类的各种内容,进行了反编译。对于JAVA这种先编译再运行的语言来说,反射机制可以使代码更加灵活,更加容易实现面向对象。
缺点:
- 性能开销:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的Java代码要慢很多。
- 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
- 内部暴露:由于反射允许代码执行一些在正常情况下不被允许的操作(比如:访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
使用反射来获取Class对象的三种方法?
- 通过运行时类来获取class对象:Class dataClass = Data.class;
- 通过运行时类的对象来获取class对象:Data data = new Data(); Class aClass = data.getClass();
- 通过Class的静态方法forName(String path) :Class aClass1 = Class.forName(“com.baizhi.Data”);
如何使用反射破坏单例?
class Singleton{
private static Singleton instance = new Singleton();
private Singleton(){
System.out.println("私有构造器被调用");
}
public static Singleton getInstance(){
return instance;
}
}
public class Test {
public static void main(String[] args) throws Exception {
Singleton instance1 = Singleton.getInstance();
//1.首先拿到万能的Class对象(有3种方法)
Class<Singleton> clazz=Singleton.class;
//2.然后拿到构造器,使用这个方法私有的构造器也可以拿到
Constructor<Singleton> constructor=clazz.getDeclaredConstructor();
//3.设置在使用构造器的时候不执行权限检查
constructor.setAccessible(true);
//4.由于没有了权限检查,所以在Singleton类外面也可以创建对象了,然后执行方法
//观察控制台,私有构造器又被调用了一次,单例模式被攻陷了,执行方法成功。
Singleton instance2 = constructor.newInstance();
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}
反射在项目中的应用?
RPC项目中使用动态代理屏蔽网络传输的细节时,使用JDK动态代理的过程中用到了反射。
动态代理和静态代理
代理就是通过代理对象去访问实际的目标对象,比如我们在生活中租房,可以直接找房东,也可以通过某些租房平台去租房,通过租房平台的这种方式就是代理。在java中这种租房平台就被叫做代理类,代理类不仅能实现目标对象,还能增加一些额外的功能。java中的代理方式有**静态代理**和**动态代理**。
静态代理
静态代理就是在代码运行之前,这个代理类就已经存在了
动态代理
** 动态代理是指代理类不是写在代码中,而是在运行过程中产生的,java提供了两种实现动态代理的方式,分别是基于Jdk的动态代理**和基于Cglib的动态代理。
谈一谈关于泛型的理解?
什么是泛型?
就是允许在定义类、接口时通过一个标识表示类中某个属性、某个方法的返回值、方法参数的类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)。
public T add(T x, T y){ return x + y; }
泛型实现的原理?
**原理:**编译器生成字节码的时候将泛型的类型去掉,使用泛型的时候再加上,这个过程就是类型擦除。
为什么要用泛型?(泛型的好处)
- 适用于多种数据类型执行相同的代码
private static int add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
private static float add(float a, float b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
private static double add(double a, double b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法;通过泛型,我们可以复用为一个方法:
private static <T extends Number> double add(T a, T b) {
System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
return a.doubleValue() + b.doubleValue();
}
- 泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型)
List list = new ArrayList();
list.add("xxString");
list.add(100d);
list.add(new Person());
我们在使用上述list中,list中的元素都是Object类型(无法约束其中的类型),所以在取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现java.lang.ClassCastException异常。
引入泛型,它将提供类型的约束,提供编译前的检查:
List<String> list = new ArrayList<String>();
// list中只能放String, 不能放其它类型的元素
注意点:
- 泛型的类型必须是类,不能是基本数据类型。需要用到基本数据类型的位置,拿包装类替换。
- 如果实例化时,没指明泛型的类型。默认类型为Java.lang.Object类型。
- 静态方法中不能使用类的泛型。(在Java中泛型只是一个占位符,必须在传递类型后才能使用就泛型而言,类实例化时才能正真的的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数静态的方法就已经加载完成了)
- public static void test(T obj){}//可以
- public static void test(T obj){}//不可以
- 异常类不能用泛型。
Java泛型是什么时候确认是哪种类型的?
泛型只存在于Java的编译期,在Java的运行期(已经生成字节码文件后)是会被擦除(类型擦除)的,这个期间并没泛型的存在。在对象进入和离开方法的边界处添加类型检查和类型转换的方法。用户使用该类的时候,才把类型明确下来。
序列化和反序列化
什么是序列化和反序列化?
序列化:将对象转化成可传输的二进制字节流
反序列化:将可传输的二进制字节流还原成对象
实际开发中有哪些场景需要用到序列化和反序列化?
- 对象在进行网络传输(比如远程方法调用RPC)之前需要被序列化,接收到序列胡对象之后 需要反序列化;
- 对象在存储到文件中的时候需要序列化,从文件中读出对象的时候需要反序列化;
- 对象在存储到数据库(如Redis)中的时候需要序列化,从数据库中读出对象的时候需要反序列化;
序列化的选择考虑的维度有哪些?
- 安全性
JDK原生序列化就存在安全漏洞。
- 序列化的速度
如果序列化的频率非常高,那么选择序列化速度快的协议会为你的系统性能提升不少。
- 序列化生成的体积
如果频繁的在网络中传输的数据那就需要数据越小越好,小的数据传输快,也不占带宽,也能整体提升系统的性能,因此序列化生成的体积就很关键了。
- 协议是否支持跨平台
如果一个大的系统有好多种语言进行混合开发,那么就肯定不适合用有语言局限性的序列化协议,比如 JDK 原生、Kryo 这些只能用在 Java 语言范围下,你用 JDK 原生方式进行序列化,用其他语言是无法反序列化的。
SPI机制
什么是SPI机制?
SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦。
SPI整体机制图如下:
当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的工具类是:java.util.ServiceLoader。
SPI机制的应用?
- SPI机制 - JDBC DriverManager
在JDBC4.0之前,我们开发有连接数据库的时候,通常会用Class.forName(“com.mysql.jdbc.Driver”)这句先加载数据库相关的驱动,然后再进行获取连接等的操作。
而JDBC4.0之后不需要用Class.forName(“com.mysql.jdbc.Driver”)来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的SPI扩展机制来实现。
- JDBC接口定义
首先在java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商来提供的。
- mysql实现
在mysql的jar包mysql-connector-java-6.0.6.jar中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。
- postgresql实现
同样在postgresql的jar包postgresql-42.0.0.jar中,也可以找到同样的配置文件,文件内容是org.postgresql.Driver,这是postgresql对Java的java.sql.Driver的实现。
- 使用方法
上面说了,现在使用SPI扩展来加载具体的驱动,我们在Java中写连接数据库的代码的时候,不需要再使用Class.forName(“com.mysql.jdbc.Driver”)来加载驱动了,而是直接使用如下代码:
String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
Connection conn = DriverManager.getConnection(url,username,password); .....
SPI机制的简单示例?
我们现在需要使用一个内容搜索接口,搜索的实现可能是基于文件系统的搜索,也可能是基于数据库的搜索。
public interface Search {
public List<String> searchDoc(String keyword);
}
public class FileSearch implements Search{
@Override
public List<String> searchDoc(String keyword) {
System.out.println("文件搜索 "+keyword);
return null;
}
}
public class DatabaseSearch implements Search{
@Override
public List<String> searchDoc(String keyword) {
System.out.println("数据搜索 "+keyword);
return null;
}
}
resources接下来可以在resources下新建META-INF/services/目录,
然后新建接口全限定名的文件:com.cainiao.ys.spi.learn.FileSearch,
里面加上我们需要用到的实现类
com.cainiao.ys.spi.learn.FileSearch
public class TestCase {
public static void main(String[] args) {
ServiceLoader<Search> s = ServiceLoader.load(Search.class);
Iterator<Search> iterator = s.iterator();
while (iterator.hasNext()) {
Search search = iterator.next();
search.searchDoc("hello world");
}
}
}
可以看到输出结果:文件搜索 hello world
如果在com.cainiao.ys.spi.learn.Search文件里写上两个实现类,那最后的输出结果就是两行了。
这就是因为ServiceLoader.load(Search.class)在加载某接口时,会去META-INF/services下找接口的全限定名文件,再根据里面的内容加载相应的实现类。
这就是spi的思想,接口的实现由provider实现,provider只用在提交的jar包里的META-INF/services下根据平台定义的接口新建文件,并添加进相应的实现类内容就好。