Java基础面试题

159 阅读40分钟

Java如何从源代码转换成机器码执行的?

.java文件.class文件(JVM可理解的Java字节)机器可执行的二进制机器码
用Javac编译器将Java源代码编译成我们常见的.class文件,然后程序在运行时,JVM将需要用到的.class文件加载到内存中,从class文件中逐行读出一条指令,JVM中解释器解释一条指令,接着执行一条指令,如我们上面所说,边解释边执行,从而达到将.class文件翻译为机器码。

如何理解JAVA中的一次编译处处运行?

Java编译器将.Java文件编译成字节码(.class文件),class文件中的代码是一种中间代码,介于源程序与机器码之间,该字节码不能被除JVM外任何平台所理解,当JVM接收到字节码,它会识别出它所工作的平台,然后将字节码转换为原生的机器码。

JDK和JRE的区别?

  1. **JDK:**是Java开发工具包,是开发人员所需要安装的环境。包含Java虚拟机和Java基础库。
  2. **JRE:**是Java运行时环境,Java运行所需要安装的环境,它不能创建程序。
  3. JDK和JRE内部都有JVM。

如果只是为了运行程序,那么只需装JRE即可,如果需要编程那么需要装JDK。但是有时候不在计算机进行编程也需要装JDK,因为有些其他软件需要JDK环境的支持(比如:Neo4j,在使用Neo4j图数据库前,需要预装JDK环境, 因为Neo4j是用Java编写的。使用JSP来部署Web应用程序也需要JDK,因为应用服务程序服务器会将JSP转换为Java Servlet,需要使用JDK来编译Servlet)。

Java和C++的异同?

** **JavaC++
面向对象的特点都支持继承、封装和多态
有无指针不提供指针来访问内存提供指针来访问内存
是否自动回收垃圾有自动垃圾回收机制GC没有自动垃圾回收机制,需要程序员手动释放
是否支持多继承不支持多继承,通过实现接口可以间接实现多继承支持多继承

值传递和引用传递的区别?

值传递:在调用函数的时候,将实参复制一份直接传递到函数中,如果函数对形参进行修改的时候,不会影响到实参。
引用传递:在调用函数的时候,将实参的地址传递到函数中,如果函数对形参进行修改的时候,会影响到实参的值。
image.png

Java中为什么只有值传递?

blog.csdn.net/bjweimengsh…

image.png
稍微解释下这张图,当我们在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,但是实际上并没有。
通过概念我们也能知道,这里是把实际参数的引用的地址复制了一份,传递给了形式参数。所以,上面的参数其实是值传递,把实参对象引用的地址当做值传递给了形式参数。

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'}

什么是隐式转换和显示转换?

  1. 隐式转换是自动类型转换:小类型的数据类型自动接受大类型的数据类型。
  2. 显示转换是强制类型转换:把一个大类型的数据强制转换成小类型的数据。

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中基本数据类型?

数据类型位数(字节数)默认值取值范围
byte8(1)0-2^7~2^7-1
short16(2)0-2^15~2^15-1
int32(4)0-2^31~2^31-1
long64(8)0-2^63~2^63-1
float32(4)0.0-2^31~2^31-1
double64(8)0.0-2^63~2^63-1
char16(2)0~2^16-1
boolean8(1)falsetrue、false

基本类型和包装类型的区别?

** **基本类型包装类型
是否可以用于泛型不可以用于泛型可以用于泛型
是否可以为null不可以为null可以为null
判断是否相等只能用“==”来比较两个数据的大小“==”用来比较包装类型的地址是否相同,equals()方法如果没有重写,比较的是地址,如果重写了那么比较的是内容。

为什么基本类型不能作为泛型?

比如,我们没有ArrayList,只有ArrayList, 为何?
因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储int值,只能引用Integer的值。
另外需要注意,我们能够使用list.add(1)是因为Java基础类型自动装箱拆箱操作。

自动装箱与自动拆箱?为什么要装箱?

  1. **自动装箱:**将八种基本数据类型(short、byte、int、float、long、double、boolean、char)用它们对应的引用类型包装起来,装箱过程是通过调用包装器的valueOf方法实现的。
  2. **自动拆箱:**将包装类型转换成基本数据类型,拆箱过程是通过调用包装器的 xxxValue方法实现的。

为什么要装箱?
Java早年设计的一个缺陷,基本数据类型不是对象,自然不是Object的子类,需要装箱才能把数据类型变成一个类,那就可以把装箱过后的基本数据类型当做一个对象,就可以调用object子类的接口。而且基本数据类型是不可以作为形参使用的,装箱后就可以。而且在jdk1.5之后就实现了自动装箱拆箱,包装数据类型具有许多基本数据类型不具有的功能,只是装箱拆箱过程会稍稍微的影响一下效率。

缓存池

new Integer(123) 与 Integer.valueOf(123) 的区别在于:

  • new Integer(123) 每次都会新建一个对象
  • 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
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
    }
}
//原因:
public static Integer valueOf(int i) {
if(i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128];
else
       return new Integer(i);
}

valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

在 Java 8 中,Integer 缓存池的大小默认为 -128~127。

static final int low = -128;
static final int high;
static final Integer cache[];

static {
    // high value may be configured by property
    int h = 127;
    String integerCacheHighPropValue =
        sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    if (integerCacheHighPropValue != null) {
        try {
            int i = parseInt(integerCacheHighPropValue);
            i = Math.max(i, 127);
            // Maximum array size is Integer.MAX_VALUE
            h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
        } catch( NumberFormatException nfe) {
            // If the property cannot be parsed into an int, ignore it.
        }
    }
    high = h;

    cache = new Integer[(high - low) + 1];
    int j = low;
    for(int k = 0; k < cache.length; k++)
        cache[k] = new Integer(j++);

    // range [-128, 127] must be interned (JLS7 5.1.7)
    assert IntegerCache.high >= 127;
}

编译器会在缓冲池范围内的基本类型自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。

Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true

基本类型对应的缓冲池如下:

  • 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个字节占若干个字节

什么是面向对象?Java面向对象的特性?

**面向对象:**面向对象是一种思想,将功能封装进对象之中,让对象去实现具体的细节;这种思想是将数据作为第一位,而方法或者说是算法作为其次,这是对数据一种优化,操作起来更加的方便,简化了过程。

面向对象特性-封装

**封装:**利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
优点:

  1. 减少耦合: 可以独立地开发、测试、优化、使用、理解和修改。
  2. 减轻维护的负担: 可以更容易被程序员理解,并且在调试的时候可以不影响其他模块。
  3. 有效地调节性能: 可以通过剖析确定哪些模块影响了系统的性能。
  4. 提高软件的可重用性。
  5. 降低了构建大型系统的风险: 即使整个系统不可用,但是这些独立的模块却有可能是可用的。

面向对象特性-继承

继承:从已有类得到继承信息然后创建新类的过程。提供继承信息的类称为父类(超类),得到继承信息的类称为子类。
继承应该遵循
里氏替换
原则,子类对象必须能够替换掉所有父类对象。
Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 向上转型
Animal animal = new Cat();

面向对象特性-多态

多态:允许不同的子类型的对象对同一消息作出不同的响应。【调用相同名字的方法,得到的结果不同,这就是多态。】
多态分为
编译时多态和运行时多态

juejin.cn/post/690762…

什么是构造器?需要注意点有哪些?

  1. 构造器的名字和类名相同,构造器没有返回值,但是也不能用void声明。
  2. 构造方法不可被重写,可以重载
  3. 当生成类对象的时候,会自动执行,无需调用。
  4. 不能被static、final、native、abstract和synchronized修饰,不能被子类继承。
  5. 如果显式给定带参的构造器后,系统就不再提供默认的无参构造器。
  6. 子类可以通过super语句调用父类的构造方法。
  7. 在接口中不可以有构造方法。
  8. 在抽象类中必须有构造方法,只是不能直接创建抽象类的实例对象。实例化子类的时候,会先初始化父类,不管父类是不是抽象类都会调用父类的构造方法。

空参构造器的作用?

  1. 子类继承父类的时候会默认调用super(),如果父类没有显示编写有参构造器,那么会默认存在空参构造器,否则会报错。
  2. 便于反射时,创建运行时类的对象。

局部变量为什么要手动初始化?

  1. 局部变量是指类方法中的变量,必须初始化。
  2. 局部变量运行时被分配在栈中,量大,生命周期短,如果虚拟机给每个局部变量都初始化一下,是一笔很大的开销,但变量不初始化为默认值就使用是不安全的。出于速度和安全性两个方面的综合考虑,解决方案就是虚拟机不初始化,但要求编写者一定要在使用前给变量赋值。

成员变量与局部变量的区别?

成员变量局部变量
在类中的位置不同在类中方法外面在方法内或者参数列表中
数据存储位置不同在堆中(方法区中的静态区)在栈中
生命周期不同随着对象的创建而存在,随着对象的消失而消失随着方法的调用或者代码块的执行而存在,随着方法的调用完毕或者代码块的执行完毕而消失
是否有默认初始值有默认初始值没有默认初始值,使用之前需要赋值,否则编译器会报错

静态变量(类变量)与成员变量(实例变量)的区别?

静态变量成员变量
生命周期不同随着类的加载而存在随着对象的创建而存在
调用方式不同类名.变量名,JDK1.7 以后也能用对象名.变量名调用。对象名.变量名
数据存储位置不同静态变量和全局变量都存在方法区堆内存

静态方法和非静态方法(实例方法)的区别?

静态方法非静态方法
生命周期不同随着类的加载而存在随着对象的创建而存在
调用方式不同类名.方法名 or 对象名.方法名
【调用静态方法无需创建对象】
对象名.方法名
访问的限制只能访问静态变量 or 静态方法还可以访问实例变量和实例方法
是否可以使用super和this不能使用this和super(因为静态方法存在的时候不一定存在对象)可以使用this和super

如何确定一个属性是否要声明为static?如何确定一个方法是否要声明为静态方法?

  1. 属性是可以被多个对象所共享的,不会随着对象的不同而不同的。这样的属性需要声明为static。
  2. 工具类中的方法,习惯上声明为静态方法,这样可以直接通过类名.方法名来调用,不需要实例化一个对象来调用方法。 比如:Math、Arrays、Collections。

在静态方法中为什么不可以调用非静态成员?

因为不需要实例对象就可以通过类名来调用静态方法,所以静态方法里面不可以调用非静态属性或非静态方法。(生命周期不同)

抽象类中可以有静态方法吗?

抽象类不能有静态的抽象方法。抽象类是不能实例化的,即不能被分配内存。但是静态方法在类实例化之前就已经被分配内存,这样一来矛盾就出现了:抽象类不能被分配内存,而静态方法必须被分配内存。另外,定义抽象方法的目的是重写此方法,但如果定义成静态方法就不能被重写。

谈一下对于继承的理解?

Java访问修饰符的作用范围?

** **同一个类同一个包其他类其他包子类其他包其他类
private可以不可以不可以不可以
缺省可以可以不可以不可以
protected可以可以可以不可以
public可以可以可以可以

Java为什么没有多继承?

image.png
**会产生钻石危机。**我们假设类A、B、C内的方法都是 public 的,以方便讨论。
假设类A中有一个public方法fun(),然后类B和类C同时继承了类A,类B或类C中各自对fun()进行了覆盖,这时类D通过多继承同时继承了类B和类C,这样便导致砖石危机了,程序在运行的时候对应方法fun()该如何判断?看到这里,相信大家也明白了多重继承会导致这种有歧义的情况存在。

  1. 若子类继承的父类中拥有相同的成员变量,子类在引用该变量时将无法判别使用哪个父类的成员变量。
  2. 若一个子类继承的多个父类拥有相同方法,同时子类并未覆盖该方法(若覆盖,则直接使用子类中该方法),那么调用该方法时将无法确定调用哪个父类的方法。

接口是什么?为什么要使用接口而不是直接使用具体类?

  1. 接口用于定义 API,它定义了类必须得遵循的规则。它提供了一种抽象。
  2. 可以有多重实现:如 List 接口,你可以使用可随机访问的 ArrayList,也可以使用方便插入和删除的 LinkedList。
  3. 接口中不允许普通方法,以此来保证抽象,但是 Java 8 中你可以在接口声明静态方法和默认普通方法。

接口和抽象类的异同?

** **抽象类接口
修饰关键字abstractinterface
实现关键字extendsimplements
默认的方法实现抽象类可以同时包含抽象和非抽象的方法。接口中所有的方法隐含的都是抽象的。
实现子类可以不实现抽象类声明的所有方法。在这种情况下,类也必须得声明成是抽象的。子类实现父类所有的抽象方法后子类就是一个普通类。子类必须要实现接口声明的所有方法。
是否有构造器可以有构造器不能有构造器
访问修饰符抽象方法可以有public、protected和default这些修饰符接口方法修饰符必须是public
多继承一个类只能继承一个抽象类一个类可以实现多个接口
是否可以实例化不可以实例化

extends和implement 区别?

  1. extends是继承父类,只要那个类不是声明为final或者那个类定义为abstract的就能继承,
  2. 继承只能继承一个类,但implements可以实现多个接口,用逗号分开就行。

比如class A extends B implements C,D,E
interface的引入是为了部分地提供多继承的功能。在interface中只需声明方法头,而将方法体留给实现的class来做。这些实现的class的实例完全可以当作interface的实例来对待。在interface之间也可以声明为extends(多继承)的关系。

重写和重载的区别?【访问权限、返回值、方法名、形参、异常】

** **重写重载(两同三不同)
访问权限访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)不影响
返回类型返回值类型和父类相同类型或者是子类的类型不影响
方法名子类重写的方法名和父类被重写的方法名相同重载的方法名相同
参数列表参数列表必须完全与被重写的方法相同必须具有不同的参数列表(参数个数、参数顺序、参数返回类型不同)
抛出异常抛出异常的范围不大于被重新的方法的异常范围。不影响
作用范围父类和子类之间同一个类中

什么时候不能对父类的方法进行重写?

  1. 用final修饰的方法不能重写。
  2. 用private修饰的方法不能重写。
  3. 用static修饰的方法语法上子类允许出现和父类只有方法体不一样其他都一模一样的static方法,但是在父类引用指向子类对象时,通过父类引用调用的依然是父类的static方法,而不是子类的static方法。
class Father {
    public static void staticMethod() {
        System.out.println("Father");
    }
}

class Son extends Father {
    // @Override 因为从逻辑上达不到重写的目的,所以这里添加 @Override 会编译报错
    public static void staticMethod() {
        System.out.println("Son");
    }
}

class Test {
    public static void main(String[] args) {
        Father mByFather = new Father();
        Father mBySon = new Son();
        Son son = new Son();

        mByFather.staticMethod();
        mBySon.staticMethod();  // 这里返回的是Father而不是Son, static方法上重写不会报错,但是从逻辑运行效果上来看达不到多态的目的
        son.staticMethod();
    }
}

父类、子类的静态代码块和构造方法的执行顺序?

执行顺序记住两个优先:静态优先,父类优先!

public class A {
    //静态代码块
    static{
        System.out.println("父类的静态代码块");
    }
    //父类的构造方法
    public A(){
        System.out.println("父类的构造方法");
    }
}
public class B extends A {
    //子类的静态代码块
    static{
        System.out.println("子类的静态代码块");
    }
    //子类的构造方法
    public  B(){
        System.out.println("子类的构造方法");
    }
    //子类的普通方法
    public void test(){
        System.out.println("子类的普通方法");
    }
    //main方法
    public static void main(String[] args) {
        B b=new B();
        b.test();
        System.out.println("子类的main方法");
    }
}
//执行结果
父类的静态代码块
子类的静态代码块
父类的构造方法
子类的构造方法
子类的普通方法
子类的main方法

谈一下对于类图的理解?

泛化关系 (Generalization)
用来描述继承关系,在 Java 中使用 extends 关键字。
image.png
实现关系 (Realization)
用来实现一个接口,在 Java 中使用 implement 关键字。
image.png
聚合关系 (Aggregation)
表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。
image.png
组合关系 (Composition)
和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。
image.png
关联关系 (Association)
表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系,一个学校可以有很多学生,但是一个学生只属于一个学校,因此这是一种多对一的关系,在运行开始之前就可以确定。
image.png
依赖关系 (Dependency)
和关联关系不同的是,依赖关系是在运行过程中起作用的。A 类和 B 类是依赖关系主要有三种形式:
1. A 类是 B 类中的(某中方法的)局部变量;
1. A 类是 B 类方法当中的一个参数;
1. A 类向 B 类发送消息,从而影响 B 类发生变化;
image.png

谈一下对于String的理解?

String底层数据结构和实现原理?

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
  1. String 被声明为 final,因此它不可被继承。
  2. 内部使用 char 数组存储数据,该数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。
  3. String类实现了Serializable接口,说明String类可以进行序列化和反序列化。

String为什么是final?不可变的好处?

1. 可以缓存 hash 值
因为 String 的 hash 值经常被使用。例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
2. String Pool 的需要
如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
image.png
3. 安全性
String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。
4. 线程安全
String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

有什么办法能改变String的不可变性?

使用反射。

String s1 = “abc” 和String s2 = new String(“abc”)的区别?

  1. 通过字面量定义的字符串常量,数据声明在方法区中的**字符串常量池(String Pool)**中。
  2. 通过new关键字和构造器创建的字符串常量,JVM会首先检查字符串常量池:
    1. 如果该字符串已经存在常量池中,那么不在字符串常量池中创建该字符串对象,而是直接复制堆中该对象的副本,然后将堆中对象的地址赋值给引用s;
    2. 如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,然后在堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s。

String、StringBuffer 和 StringBuilder的区别?

** **StringStringBufferStringBuilder
是否可变性字符串常量,底层用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

在 Java 7 之前,字符串常量池被放在运行时常量池中,它属于永久代。而在 Java 7,字符串常量池被移到 Native Method 中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。

static关键字的作用?

static可以修饰变量、方法、代码块、内部类

  1. 静态变量属于类所有,随着类的加载而存在,所有实例对象都共享静态变量。
  2. 静态方法也属于类所有,随着类的加载而存在,可以通过【对象名.方法名】和【类名.方法名】两种方式来访问,静态方法只能调用静态方法和静态变量,不能访问非静态方法和非静态变量。
  3. 静态代码块在类被第一次加载时执行静态代码块,且只被执行一次,主要作用是实现 static 属性的初始化。
  4. 静态内部类:非静态内部类依赖于外部类的实例,而静态内部类不需要。静态内部类不能访问外部类的非静态的变量和方法。
  5. 单例模式有一种实现方式就是使用静态内部类来实现的,静态内部类中可以有静态成员,但是非静态内部类不能有静态成员。

instanceof关键字的作用?

用来测试一个对象是否是一个类的实例(不能比较基本数据类型)。
Integer integer = new Integer(20);
System.out.println(integer instanceof Integer); //输出 true

final关键字总结?

  1. 被final修饰的不能被继承。
  2. 被final修饰的方法不能被重写。使用private修饰的方法被隐式地定义为final方法。
  3. 被final修饰的数据

声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。

  • 对于基本类型,final 使数值不变;
  • 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
  1. 类中所有private方法都被隐式地指定为final。

final、finally、finalize()的区别?

  1. final可以用来修饰变量、方法、类。final修饰变量,该变量是常量不可改变;final修改方法,该方法不可被重写;final修饰类,该类不可被继承。
  2. finally是一个Java关键字,和try块一起使用,try….finally的代码块,finally代码块里面的内容一定会执行。像数据库连接、输入输出流、网络编程Socket等资源,JVM是不能自动的回收的,我们需要自己手动的进行资源的释放。此时的资源释放,就需要声明在finally中。
  3. finalize()是Object类中的方法,使用finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。

abstract关键字的作用?

  1. 被abstract修饰的方法是抽象方法,只有方法声明没有方法体;
  2. 类中只要有一个抽象方法,这个类就是抽象类;
  3. 抽象类中的方法可以有抽象方法,也可以有非抽象方法;
  4. 一个类只有把抽象类中的方法全部实现,那么这个类才算是普通类,否则它还是抽象类;
  5. 抽象类不能被实例化;

谈一谈关于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()方法

  1. **对称性:**如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true”
  2. **自反性:**x.equals(x)必须返回是“true”
  3. **传递性:**如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也应该返回是“true”
  4. **一致性:**如果x.equals(y)返回是“true”,只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是“true”
  5. **非空性:**对于任意非空引用x,x.equals(null),永远返回是“false”

==和equals()的区别?基本数据类型与String类型使用==和equals()的注意事项?

  1. 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
  2. 对于引用类型,== 判断两个变量是否引用同一个对象,而 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方法?

  1. 如果只重写equals,不重写hashCode会怎样?

如果需要大量并快速对比对象的话,equals 效率太低了。 这种容器一般为:hashset、hashmap、hashtable等,如:hashset 要求对象不能重复,内部肯定会对每个对象进行比较,所以根据对比规则,如果 hashCode 相同,在用 equals 验证,如果 hashCode 不同,则不同,这样就提高了效率

  1. 只重写 hashCode 会怎样呢?

在Java中的一些容器中,不允许有两个完全相同的对象,插入的时候,如果判断相同则会进行覆盖。这时候如果只重写了equals()的方法,而不重写hashcode的方法,Object中hashcode是根据对象的存储地址转换而形成的一个哈希值。这时候就有可能因为没有重写hashcode方法,造成相同的对象散列到不同的位置而造成对象的不能覆盖的问题。

String类的hashCode与equals是怎么重写的?

String的equals():[地址是否相同->是否为同一对象的实例->长度或大小->每个元素的值是否相同]

  1. 首先判断两个对象的地址是否相同,如果相同直接返回true。
  2. 否则继续判断,判断两个对象是否来自同一个对象的实例,如果不是直接返回false。
  3. 否则继续判断,判断两个String对象的长度是否相同,如果不相同直接返回false。
  4. 如果相同继续判断,判断每个位置的值是否相同,如果有一个不相同直接返回false。
  5. 执行完所有语句,如果没有返回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的集合中是怎么判断两个对象是否相等的?

  1. 首先判断两个对象的地址是否相同,如果相同直接返回true。
  2. 否则继续判断,判断两个对象是否来自同一个对象的实例,如果不是直接返回false。
  3. 否则继续判断,判断两个集合对象的size是否相同,如果不相同直接返回false。
  4. 如果相同继续判断,判断每个元素是否相同,如果遇到不相同直接返回false。
  5. 执行完所有语句,如果没有返回false,那么返回true。

谈一下对于异常的理解?

Java的异常体系?

image.png

Java错误(Error)和异常(Exception)的区别?

Java将所有的错误封装为一个对象,其根本父类为Throwable,Throwable有两个子类:Error和Exception。

  1. **Error错误:**程序中无法处理的错误,表示运行应用程序中出现严重错误。一般是虚拟机出现错误,无法处理需要终止程序,比如OOM和栈溢出。
  2. **Exception异常:**程序本身可以处理的异常,异常分为两类:运行时异常和编译时异常。

Exception中的编译时异常和运行时异常的区别?

  1. **运行时异常(RuntimeException类及其子类异常):**都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
  2. 编译时异常(Exception中除RuntimeException以外的异常):编译时异常是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常。

Java异常中检查时异常和非检查异常区别?

  1. **检查时异常(Exception中的编译时异常):**是编译器要求必须处理的异常。当编译器检查到应用中的某处可能会此类异常时,将会提示你处理本异常,要么使用try-catch捕获,要么使用throws 关键字抛出,否则编译不通过。
  2. **非检查时异常(Error和Exception中运行时异常):**编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有try-catch捕获它,也没有使用throws抛出该异常,编译也会正常通过。

异常总结

  1. try、catch和finally都不能单独使用,只能是try-catch、try-finally或者try-catch-finally。
  2. try语句块监控代码,出现异常就停止执行下面的代码,然后将异常移交给catch语句块来处理。
  3. finally语句块中的代码一定会被执行,常用于回收资源 。
  4. throws:声明一个异常,告知方法调用者。
  5. throw :抛出一个异常,至于该异常被捕获还是继续抛出都与它无关。

谈一谈关于注解的理解?

什么是注解?

注解在我的理解下,就是代码中的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相对应的处理。

你在开发中有没有用到注解?

  1. 注解其实在开发中是非常常见的,比如我们在使用各种框架时(像我们Java程序员接触最多的还是Spring框架一套),就会用到非常多的注解,@Controller / @Param / @Select 等等。一些项目也用到lombok的注解,@Slf4j / @Data 等等
  2. 除了框架实现的注解,Java原生也有@Overried、@Deprecated、@FunctionalInterface等基本注解。Java原生的基本注解大多数用于「标记」和「检查」
  3. 原生Java除了这些提供基本注解之外,还有一种叫做元Annotation(元注解),所谓的元Annotation就是用来修饰注解的。常用的元Annotation有**@Retention 和@Target**。
    1. @Retention注解可以简单理解为设置注解的生命周期
    2. @Target表示这个注解可以修饰哪些地方(比如方法、成员变量、包等等)

自定义注解在项目中的应用?

@Retention注解传入的是RetentionPolicy枚举,该枚举有三个常量,分别是SOURCE、CLASS和RUNTIME

  1. SOURCE代表着注解仅保留在源级别中,并由编译器忽略。
  2. CLASS代表着注解在编译时由编译器保留,但Java虚拟机(JVM)会忽略。
  3. RUNTIME代表着标记的注解会由JVM保留,因此运行时环境可以使用它。

一般来说,只要自定义的注解中@Retention注解设置为SOURCE和CLASS这俩个级别,那么就需要继承并实现AbstractProcessor(因为SOURCE和CLASS这俩个级别等加载到jvm的时候,注解就被抹除了)
lombok的实现原理就是在这(为什么使用了个@Data这样的注解就能有set/get等方法了,就是在这里加上去的)

我们自己定义的注解都是RUNTIME级别的,因为大多数情况我们是根据运行时环境去做一些处理。

RPC项目的应用

  1. 服务提供者需要在暴露的服务上增加注解 @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上。

  1. 消费服务需要使用自定义的 @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 方法。


谈一谈对于反射的理解?

什么是反射?

  1. 反射就是把java类中的各种成分映射成一个个的Java对象。例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把个个组成部分映射成一个个对象。
  2. 反射能够在运行时获取类的信息;
  3. JVM加载完类后,会在堆内存的方法区中产生一个Class类型的对象(一个类只有一个Class对象),这个Class对象包含了完整的了结构信息。(这个对象就像一面镜子,通过这个镜子能够看到类的结构,这种方法就是反射。)

image.png

为什么要在运行时获取类的信息? .java文件 -> .class文件 -> 加载到JVM运行,这就是所谓的运行时 在运行时获取类的信息,其实就是为了让我们所写的代码更具有「通用性」和「灵活性」

反射在框架中的应用?

  1. SpringMVC 你在方法上写上对象,传入的参数就会帮你封装到对象上
  2. Mybatis可以让我们只写接口,不写实现类,就可以执行SQL【动态代理->反射】
  3. 在类上加上@Component注解,Spring就帮你创建对象
  4. JDK动态代理其实就是运用了反射的机制,而CGLIB代理则用的是利用ASM框架,通过修改其字节码生成子类来处理
  5. 使用JDBC连接数据库时使用**Class.forName()**通过反射加载数据库的驱动程序。

反射机制的作用?

  1. 在运行时判断任意一个对象所属的类。
  2. 在运行时构造任意一个类的对象。
  3. 在运行时判断或调用任意一个类所具有的成员变量和方法。
  4. 在运行时获取泛型信息。

反射机制的优缺点?

  1. 优点:可以实现动态创建对象和编译,体现出很大的灵活性。通过反射机制我们可以获得类的各种内容,进行了反编译。对于JAVA这种先编译再运行的语言来说,反射机制可以使代码更加灵活,更加容易实现面向对象。
  2. 缺点:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的Java代码要慢很多

使用反射来获取Class对象的三种方法?

  1. 通过运行时类来获取class对象:Class dataClass = Data.class;
  2. 通过运行时类的对象来获取class对象:Data data = new Data(); Class aClass = data.getClass();
  3. 通过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动态代理的过程中用到了反射。


谈一谈关于泛型的理解?

什么是泛型?

就是允许在定义类、接口时通过一个标识表示类中某个属性、某个方法的返回值、方法参数的类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)。
public T add(T x, T y){ return x + y; }

泛型实现的原理?

**原理:**编译器生成字节码的时候将泛型的类型去掉,使用泛型的时候再加上,这个过程就是类型擦除。

为什么要用泛型?(泛型的好处)

  1. 适用于多种数据类型执行相同的代码
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();
}
  1. 泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型)
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, 不能放其它类型的元素

注意点:

  1. 泛型的类型必须是,不能是基本数据类型。需要用到基本数据类型的位置,拿包装类替换。
  2. 如果实例化时,没指明泛型的类型。默认类型为Java.lang.Object类型。
  3. 静态方法中不能使用类的泛型。(在Java中泛型只是一个占位符,必须在传递类型后才能使用就泛型而言,类实例化时才能正真的的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数静态的方法就已经加载完成了
    1. public static void test(T obj){}//可以
    2. public static void test(T obj){}//不可以
  4. 异常类不能用泛型。

Java泛型是什么时候确认是哪种类型的?

泛型只存在于Java的编译期,在Java的运行期(已经生成字节码文件后)是会被擦除(类型擦除)的,这个期间并没泛型的存在。在对象进入和离开方法的边界处添加类型检查和类型转换的方法。用户使用该类的时候,才把类型明确下来。