面试中常被问到的的 Java 基础相关的问题!

80 阅读16分钟

面向对象的特征?

封装、继承、多态。

说说 Java 多态的实现原理

多态机制包括静态多态(编译时多态)和动态多态(运行时多态)。 我们通常所说的多态一般指运行时多态,也就是编译时不确定究竟调用哪个具体方法,一直等到运行时才能确定。

多态核心之处就在于对父类方法的改写或对接口方法的实现,以取得在运行时不同的执行效果。

Java 里对象方法的调用是依靠类信息里方法表现的,对象方法引用调用和接口方法引用调用的大致思想是一致的。当调用到对象的某个方法时,JVM 会查找该对象的类的方法表来确定该方法的直接引用地址,有了地址后才能真正调用该方法。

静态多态

静态多态比如说重载。

动态多态

动态多态一般指在运行时才能确定调用哪个方法。

多态的实现方式

  1. 子类继承父类
  2. 类实现接口

重载与重写的区别?

重载

  1. 重载表示同一个类中可以有多个名称相同的方法,但是这些方法的参数列表各不相同(参数的个数和类型)。

重写

  1. 重写必须继承,而重载不用
  2. 重写表示子类中的方法与父类中的某个方法名称和参数完全相同。通过子类实例对象调用这个方法的时候,将调用子类中定义的方法,相当于把父类中定义的那个完全相同的方法给覆盖了,这是面向对象编程的多态性的一种表现。
  3. 重写的方法的修饰符,必须大于等于父类的方法(即访问权限只能比父类的更大,不能比父类小),而重载则于修饰符无关。
  4. 重写覆盖的方法,只能比父类抛出更少的异常或者是抛出父类抛出的异常的子异常。

抽象类和接口有什么区别?

  1. 抽象类要被子类继承,接口是被子类实现。
  2. 抽象类可以有构造方法,接口中不能有构造方法。
  3. 抽象类中可以有普通成员变量,接口中没有普通成员变量,接口中的变量只能时公共的静态的常量。
  4. 一个类可以实现多个接口,但是只能继承一个类。
  5. 接口中只能做方法声明,而抽象类中可以做方法的声明也能做方法的实现。
  6. 抽象级别:接口 > 抽象类 > 实现类。
  7. 抽象类主要用来抽象类别,而接口主要用来抽象方法功能。
  8. 抽象类的关键字时 abstract,而接口的关键字是 interface。

接口是否可以继承接口?抽象类是否可以实现接口?抽象类是否可继承实体类?

都是可以的。

静态内部类和非静态内部类有什么区别?

  1. 静态内部类中可以有静态成员(方法、属性),而非静态内部类中不能有静态成员。
  2. 静态内部类只能访问外部类的静态成员和静态方法,而非静态内部类可以访问外部类中的所有成员。
  3. 调用内部静态类的方法或者静态变量,可以通过类名直接调用。
  4. 实例化静态内部类和非静态内部类的方式不同。

juejin.cn/post/684490…

可以在 static 环境中访问非 static 变量吗?

不可以。

static 变量在 Java 中是属于类的,它在所有的实例中都是一样的。当类被虚拟机加载的时候,会对 static 变量进行初始化。

因为静态的成员属于类,会随着类的加载而加载到静态方法区内存。当类加载的时候,此时不一定有实例创建,没有实例就不可能访问到非 static 成员变量。且类的加载优先于实例的创建,因此在 static 环境中,不可以访问非静态成员变量。

说下类的实例化顺序,比如:父类静态数据、构造函数、子类静态数据、子类构造函数

类实例化顺序如下:

父类静态代码块/静态域 > 子类静态代码块/静态域 > 父类非静态代码块 -> 父类构造器 -> 子类非静态代码块 -> 子类构造器。

深拷贝与浅拷贝的区别?

浅拷贝

浅拷贝复制了对象的引用地址,两个对象指向的是同一个地址,所以修改其中任意的值另一个也会随之变化。

深拷贝

深拷贝将对象以及对象的值都复制过来了,两个对象修改其中的任意的值,另一个值不会发生改变。

如何实现对象克隆?

  1. 实现 Cloneable 接口,重写 clone() 方法。
  2. Object 的 clone() 方法是浅拷贝(如果类中属性有自定义引用类型,只拷贝引用而不拷贝引用所指的对象)。
  3. 对象的属性的 Class 也实现了 Cloneable 接口,在克隆对象时也手动克隆属性完成深拷贝。
  4. 结合序列化完成深拷贝(JDK java.io.Serializable 接口、JSON 格式、XML 格式等)。

什么是值传递和引用传递?

值传递

值传递是对基本型变量而言的,传递的是该变量的一个副本,该变副本不影响原变量。

引用传递

引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本, 并不是原对象本身 。所以对引用对象进行操作会同时改变原对象。

JDK 和 JRE 的区别?

JDK

Java Development Kit 的简称,Java 开发工具包,提供了 Java 的开发环境和运行环境。

JRE

Java Runtime Environment 的简称,Java 运行环境,为 Java 的运行提供了所需环境。

Java 创建对象有几种方式

有 5 种.

  1. 用 new 语句创建对象。
  2. 使用反射,使用 Class.newInstance() 创建对象,调用类对象的构造方法 Constructor。
  3. 调用对象的 clone() 方法。
  4. 运用反序列化手段,调用 java.io.ObjectInputStream 对象的 readObject() 方法。
  5. 使用 Unsafe

Java 泛型和类型擦除

juejin.cn/post/684490…

Java泛型常见几道面试题

  • Java中的泛型是什么 ? 使用泛型的好处是什么?(第一,第二小节可答)
  • Java的泛型是如何工作的 ? 什么是类型擦除 ? (第四小节可答)
  • 什么是泛型中的限定通配符和非限定通配符 ? (第三小节可答)
  • List<? extends T>和List <? super T>之间有什么区别 ?(第三小节可答)
  • 你了解泛型通配符与上下界吗?(第三小节可答)

什么是 Java 泛型

Java 泛型是 JDK 5 中引入的一个新特性,其本质是参数化类型,主要是解决不确定具体对象类型的问题。其所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

泛型类

泛型类(generic class) 就是具有一个或多个类型变量的类。一个泛型类的简单例子如下:

// 常见的如T、E、K、V等形式的参数常用于表示泛型,编译时无法知道它们类型,实例化时需要指定。
public class Pair <K,V>{
    private K first;
    private  V second;

    public Pair(K first, V second) {
        this.first = first;
        this.second = second;
    }

    public K getFirst() {
        return first;
    }

    public void setFirst(K first) {
        this.first = first;
    }

    public V getSecond() {
        return second;
    }

    public void setSecond(V second) {
        this.second = second;
    }

    public static void main(String[] args) {
    // 此处K传入了Integer,V传入String类型
        Pair<Integer,String> pairInteger = new Pair<>(1, "第二");
        System.out.println("泛型测试,first is " + pairInteger.getFirst()
                + " ,second is " + pairInteger.getSecond());
    }
}

泛型接口

泛型也可以应用于接口。实现类去实现这个接口的时候,可以指定泛型T的具体类型。

public interface Generator<T> {
    T next();
}

泛型方法

具有一个或多个类型变量的方法,称之为泛型方法。

public class GenericMethods {

    public <T> void f(T x){
        System.out.println(x.getClass().getName());
    }

    public static void main(String[] args) {
        GenericMethods gm = new GenericMethods();
        gm.f("字符串");
        gm.f(666);
    }
}

泛型擦除-类型擦除

Class c1 = new ArrayList<Integer>().getClass();
Class c2 = new ArrayList<String>().getClass();
System.out.println(c1 == c2);

/* Output
true
*/

ArrayList 和 ArrayList 很容易被认为是不同的类型。但是这里输出结果是 true,这是因为 Java泛型是使用擦除实现的,不管是 ArrayList() 还是 new ArrayList(),在编译生成的字节码中都不包含泛型中的类型参数即都擦除成了ArrayList,也就是被擦除成“原生类型” ,这就是泛型擦除。

泛型通配符

我们定义泛型时,经常碰见T,E,K,V,?等通配符。本质上这些都是通配符,是编码时一种约定俗成的东西。当然,你换个 A-Z 中另一个字母表示没有关系,但是为了可读性,一般有以下定义:

  • ? 表示不确定的 java 类型
  • T (type) 表示具体的一个 java 类型
  • K V (key value) 分别代表 java 键值中的 Key Value
  • E (element) 代表 Element

上边界限定通配符 <? extends E>

  • 使用 <? extends Fruit> 形式的通配符,就是上边界限定通配符。 extends关键字表示这个泛型中的参数必须是 E 或者 E 的子类。

下边界限定通配符 <? super E>

  • 使用 <? super E> 形式的通配符,就是下边界限定通配符。 super 关键字表示这个泛型中的参数必须是所指定的类型 E,或者是此类型的父类型,直至 Object。

泛型的好处

Java 语言引入泛型的好处是安全简单。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

Java 中的基本数据类有哪些?各占用多少字节?

基本类型位数字节
int324
short162
long648
byte81
char162
float324
double648
boolean

对于 boolean, 官方文档未明确定义,它依赖于 JVM 厂商的具体实现。逻辑 上理解是占用 1 位,但是实际中会考虑计算机高效存储因素。

int 和 Integer 有什么区别?Integer 的缓存的实现?

  1. int 是基本数据类型,Integer 是 int 的封装类属于引用类型。
  2. int 默认值是 0,而 Integer 默认值是 null,所以 Integer 需要判空处理。
  3. Integer 的缓存机制:为了节省内存和提高性能,Intefer 类在内部通过使用相同的对象的引用实现缓存和重用。Integer 类默认在 -128~127 之间,可以通过 -XX:AutoBoxCacheMax 进行修改,这种机制在自动装箱的时候有用,在使用构造器创建 Integer 对象时无用。

Object 中有哪些方法

getClass() 获取类结构信息

hashcode() 获取哈希码

equals() 默认比较对象的地址值是否相等,子类可以重写比较规则

clone() 用于对象克隆

toString() 把对象转变成字符串

notify() 多线程中唤醒功能

notifyAll() 多线程中唤醒所有等待线程的功能

wait(long) 让持有对象锁的线程进入等待

wait(long,int) 让持有对象锁的线程进入等待,设置超时毫秒数时间

finalize() 垃圾回收前执行的方法

equals 与 == 的区别

==

如果是基本类型,==判断它们的值是否相等

equals

  1. 如果是字符串,会判断字符串的内容是否相等
  2. 如果是 Object 对象中的 equals 方法,比较的是引用的内存地址是否相等
  3. 如果自己重写了 eqauls 方法,可以自定义两个对象是否相等

什么时候需要重写 equals,重写 euqals 为什么必须得重写 hashCode 方法

为什么要重写 equals

Object 提供的 equals 在进行比较的时候,并不是进行值比较,而是内存地址的比较。在我们的业务系统中,要使用 equals 对对象进行比较,原生的equals方法就不能满足我们的需求。

为什么必须得重写 hashcode

以 Java.lang.Object 来理解, JVM 每次 new 一个 Object,会先根据哈希算法计算出该对象的 hashcode 值, 放入到对应的 Hash 表对应的 Key 上,如果不同的对象确产生了相同的 hashcode ,也就是发生了哈希冲突,那么就在这个 Hash key 的地方产生一个链表,所有 hashcode 相同 的对象会放到这个单链表上去,串在一起。在下次对 Object 做比较或者取这个对象的时候, 它会先根据对象的 hashcode 从 Hash 表中取这个对象。首先会根据对象的 hashcode 找到对应在 Hash 表中的位置,然后再根据 Object 的 equals 去比较这两个对象是否相等。

两个对象的 hashcode() 相同,则 equals() 是否也一定为 true

两个对象的 equals() 相等,则它们的 hashcode() 必须相等,如果两个对的 hashcode() 相等,其 equals() 不一定相等。

hashcode 的常规协定

  1. 在 java 应用程序执行中,在对同一对象进行多次调用 hashcode() 方法时,必须一致地返回相同的整数。前提时将对象进行 equals() 比较所用的信息没有被修改。但是从某一次应用程序的一次执行到同一应用的另一次执行,该整数无需保持一致。
  2. 两个对象的 equals() 相等,那么对这两个对象中的每个对象调用 hashcode() 方法都必须生成相同的整数结果。
  3. 两个对象的 equals() 方法不相等,那么对这两个对象中的任一对象上调用 hashcode() 方法不要求一定生成不同的整数结果。但是,为不相等的对象生成不同整数结果可以提供哈希表的性能。

& 和 && 的区别

  1. & 是按位与,a&b 表示把 a 和 b 转换成二进制数,再进行与运算。
  2. & 和 && 都是逻辑运算符号,其中 && 又叫短路运算符。
  3. 逻辑与,a&& b ,a&b 都表示当且仅当两个操作数均为 true 时,其结果才为 true,否则为 false。
  4. 逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都 是 true,整个表达式的值才是 true。但是,&&之所以称为短路运算,是因为如果 &&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。

Comparator 和 Comparable 有什么区别?

在 Java 语言中,Comparable 和 Comparator 都是用来进行元素排序的,但二者有着本质的区别:

  • Comparable 是“比较”的意思,而 Comparator 是“比较器”的意思;
  • Comparable 是通过重写 compareTo 方法实现排序的,而 Comparator 是通过重写 compare 方法实现排序的;
  • Comparable 必须由自定义类内部实现排序方法,而 Comparator 是外部定义并实现排序的。

所以用一句话总结二者的区别:Comparable 可以看作是“对内”进行排序接口,而 Comparator 是“对外”进行排序的接口。

String、StringBuffer、StringBuilder 的区别?

String

  1. String 类是一个不可变的类,一旦创建就不可以修改。
  2. String 是 final 的,不能被继承。
  3. String 实现了 equals() 方法和 hashCode() 方法。

String 默认的最大长度是 2^31-1,最大能存储 4GB 的字符串,也就是说我们需要有大于 4GB 的 JVM 运行内存才行。

String 一般都存在 JVM 的哪块区域?

字符串在 JVM 中的存储分两种情况,一个是存储在 JVM 的堆栈中,一个是字符串常量存储在常量池中。

常量池中的字符串最大长度是 2^31 -1 ?

Java 中字符串在常量池中通过 CONSTANT_UTF 类型表示,如下图:

我们只要重点关注 bytes[length] 即可,length 在这里就是代表字符串的长度,length 的类型是 u2,u2 是无符号的 16 位整数,也就是说最大长度可以做到 2^16-1 即 65535。

按照你说的,我在我的机器上试了一下65535长度的字符串,编译报错了。这是怎么回事呢?

因为 javac 编译器做了限制,需要 length < 65535,所以字符串常量在常量池中的最大长度是 65534。

总结:

  • 字符串常量长度不能超过 65534
  • 堆内字符串的长度不能超过 2^31-1

StringBuffer

  1. 继承自 AbstractStringBuilder,是个可变类。
  2. StringBuffer 是线程安全的。
  3. 可以通过 append() 方法动态构造数据。
  4. StringBuffer 默认容量是 16,每次 append 会增长 16。

StringBuilder

  1. 继承自 AbstractStringBuilder ,是可变类。
  2. StringBuilder 是非线程安全的。
  3. 执行效率比 StringBuffer 高。

String s 和 new String 有什么区别?

String str = "whx";
String newStr = new String("whx")

String str = "abc"

先在常量池中查找有没有 ”whx“ 这个对象,如果有,就让 str 指向 ”whx";如果没有,则在常量池中新建一个 ”whx“ 对象,并让 str 指向在常量池中新建的对象 ”whx“。

String newStr = new String("abc")

该操作是在堆中建立对象 ”whx",在栈中创建队中”whx“ 对象的内存地址。如下图所示:

String s="Hello" ;和 s = s + "world"; 这两行代码执行后,原始的 String 对象中的内容是否会改变?

不会改变。因为 String 被设计成不可变的类,所以它的所有对象都是不可变对象。

String s = "a" + "b" + "c" + "d"; 创建了几个对象?

一个对象。 Java 编译器对字符串常量直接相加的表达式进行优化,不等到运行期去进行加法运算,在编译时就去掉了加号,直接将其编译成一个这些常量相连的结果。 所以 "a"+"b"+"c"+"d" 相当于直接定义一个 "abcd" 的字符串。

String 类的常用方法有哪些?

  1. indexOf():返回指定字符的索引。
  2. charAt():返回指定索引处的字符。
  3. replace():字符串替换。
  4. trim():去除字符串两端空白。
  5. split():分割字符串,返回一个分割后的字符串数组。
  6. getBytes():返回字符串的 byte 类型数组。
  7. length():返回字符串长度。
  8. toLowerCase():将字符串转成小写字母。
  9. toUpperCase():将字符串转成大写字符。
  10. substring():截取字符串。
  11. equals():字符串比较

如何将字符串反转呢?

  1. 使用 StringBuilder 或 StringBuffer 的 reverse 方法,本质都调用了它们的父类 AbstractStringBuilder 的 reverse 方法实现。
  2. 使用 chatAt 函数,倒过来输出。

用最有效率的方法计算 2 乘以 8?

将一个数左移 n 位,就相当于这个数乘以了 2 的 n 次方。那么,一个数乘以 8,只要将其左移 3 位即可。而 CPU 直接支持位运算且效率最高。

char 型变量中能不能存储一个中文汉字,为什么?

在 Java 中,char 类型占 2 个字节,而且 Java 默认采用 Unicode 编码,一个 Unicode 码是 16 位,所以一个 Unicode 码占两个字节,Java 中无论汉子还 是英文字母都是用 Unicode 编码来表示的。所以,在 Java 中,char 类型变量 可以存储一个中文汉字。

final、finally、finalize 的区别

final

用于修饰属性、方法和类,分别表示不能被赋值、方法不可被覆盖、类不可被继承。

finally

finally 是异常处理语句结构中的一部分,一般以 try-catch-finally 的形式出现,finally 的代码块表示总是被执行。

finalize

finalzie 是 Object 类中的一个方法,该方法一般用于垃圾回收器来调用,当我们调用 System.gc() 方法的时候,由垃圾回收器调用 finalize() 方法回收垃圾,但是 JVM 并不保证此方法总被调用。

说说 Java 的异常层次结构

Error

表示编译时或者系统错误,比如:虚拟机相关的错误,OOM 等。Error 是无法处理的。

Exception

表示代码异常,它能本程序本身处理,分为运行时异常和可检查的异常。

常见的 RuntimeException 异常

  1. NullPointerException 空指针异常。
  2. ArithmeticException 出现异常的运算条件,抛出此异常。
  3. IndexOutOfBoundsException 数组索引越界异常。
  4. ClassNotFoundException 找不到类异常。
  5. IllegalArgumentException 非法参数异常。

常见的 CheckedException 异常(Checked Exception 就是编译器要求你必须处置的异常)

  1. IOException 操作输入流和输出流时可能出现的异常。
  2. ClassCastException 类型转换异常。

try-catch-finally-return 的执行顺序

  1. 如果不发生异常,不会执行 catch 部分。
  2. 不管有么有发生异常,finally 块都会执行到。
  3. 即使 try 和 catch 中有 return 时,finally 仍然会执行。
  4. finally 是在 return 后面的表达式运算完后再执行的。
  5. finally 部分不用 return 了,不然就不会区执行 try 或者 catch 中的 return 了。

说说反射的用途及实现原理,Java 中实现反射的三种方法?

Java 中实现反射的三种方法

  1. 使用 Class.forName 静态方法。
  2. 使用类的.class 方法。
  3. 使用实例对象的 getClass() 方法。

反射的用途和实现原理

juejin.cn/post/684490…

反射中 Class.forName 和 ClassLoader 的区别?

Class.forName 和 ClassLoader 都可以对类进行加载。它们区别在哪里呢?

ClassLoader 负责加载 Java 类的字节码到 Java 虚拟机中。Class.forName 其实是调用了 ClassLaoder。

Class.forName 和 ClassLoader 的区别,就是在类加载的时候, class.forName 有参数控制是否对类进行初始化。

设计原则 SOLID ?

  1. 单一职责原则:一个类只做它该做的事情。
  2. 开闭原则:软件实体应对对扩展开放,对修改关闭。
  3. 依赖倒转原则:面向接口编程。
  4. 接口隔离原则:接口要小而专,绝不能大而全。
  5. 合成聚合复用原则:优先使用聚合或者合成关系复用代码。
  6. 迪米特法则:又叫最少知识原则,一个对象应当对其它对象有尽可能少的了解。

静态代理,会实时存在一个 Proxy 代理类,每增加一个 业务对象都需要手动 new 代理类,代理类写自己需要增强的逻辑。

动态代理有两种实现方式:jdk 动态代理、cglib 动态代理。

正向代理和反向代理

正向代理:

是一个位于客户端和原始服务器之间的服务器,为了从原始服务器中取得资源,客户端向代理服务器发送一个请求并且指定目标(这里的目标就是原始服务器),然后代理向原始服务器转交请求,并将获得的资源返回给客户端。

反向代理:

反向代理的实际运行方式是指以代理服务器来接受网络上的连接请求,然后将请求转发给内容网络上的服务器,并将从服务器上得到的结果返回给网络上发送连接请求的客户端。此时代理服务器对外就变现为一个服务器。

反向代理的作用:

  1. 保证内网的安全,可以使用发现代理提供 WAF 功能,阻止 Web 攻击。大型网站,通常将反向代理作为公网访问地址,Web 服务器是内网。
  2. 负载均衡,通过反向代理服务器来优化网站的负载。

nginx 的反向代理

nginx支持配置反向代理,通过反向代理实现网站的负载均衡。这部分先写一个nginx的配置,后续需要深入研究nginx的代理模块和负载均衡模块。

nginx通过proxy_pass_http 配置代理站点,upstream实现负载均衡。

JDK 动态代理

Java 原生支持的、不需要任何外部依赖、但是它只能基于接口进行代理。

CGLIB 动态代理

通过继承的方式进行代理、无论目标对象没有没实现接口都可以代理,但 是无法处理 final 的情况。