Java基础面试题

发布于:2024-05-01 ⋅ 阅读:(28) ⋅ 点赞:(0)

1、“==”和equals

“==”用于比较两个对象引用是否指向同一个内存地址;在比较基本数据类型的时候,比较的是两个变量的值;当使用"=="比较两个对象时,它会检查两个对象引用是否指向内存中的同一个对象。如果两个对象引用指向同一个对象,则返回true;否则返回false。

(Java基本数据类型:byte、short、int、long、float、double、char、boolean)

equals()方法:它是Java中定义在Object类中的方法,用于比较两个对象的内容是否相等。equals()方法的默认实现与"=="运算符的行为相同,即比较两个对象引用是否指向同一个对象。然而,很多类(如String、Integer等)会重写equals()方法,以便比较对象的实际内容。

2、“对象引用”和“对象”

对象引用和对象是两个相关但不同的概念。

  1. 对象:对象是在内存中分配的一块区域,用于存储数据和执行操作。它具有特定的数据结构和行为,并且可以通过调用方法来访问和修改其状态。对象可以是通过实例化类而创建的,也可以是通过其他方式创建的。

  2. 对象引用:对象引用是指向对象内存地址的值或变量。它是用来访问和操作对象的工具。对象引用可以被赋值给变量,作为方法的参数传递,或者存储在数据结构中。通过对象引用,我们可以间接地操作对象的属性和调用对象的方法。

区别:

  • 对象是实际存在的实体,它占据内存空间并具有数据和行为。
  • 对象引用是指向对象的一个值或变量,它允许我们访问和操作对象。
  • 一个对象可以有多个对象引用指向它,因此多个引用可以同时访问同一个对象。
  • 对象可以在内存中创建、销毁和复制,而对象引用的值可以改变,但引用本身并不直接控制对象的生命周期。

对象是实际存在的实体,而对象引用是用于访问和操作对象的工具,它们之间是通过引用关系相连的。

3、Java多继承

Java中不支持多继承,C++是支持多继承的。多继承会产生菱形继承的问题。Java中虽然不可以多继承,但是一个类可以实现多个接口,接口可以多继承。

 4、面型对象

面相对象编程、面相过程编程都是一种编程范式。

面向对象编程将计算机程序视为一组对象的集合,每个对象都有自己的状态(属性)和行为(方法)。面向对象编程(Object-Oriented Programming,OOP)的设计思想是通过创建对象、定义对象之间的关系以及交互来解决问题。

面向对象的编程主要基于以下几个概念:

  1. 类(Class):类是对象的模板或蓝图,它定义了对象的属性和行为。类可以看作是创建对象的工厂,它提供了对象的初始状态和操作。

  2. 对象(Object):对象是类的实例,它具有类定义的属性和行为。对象是面向对象编程的核心,程序的功能通过对象之间的交互来实现。

  3. 封装(Encapsulation):封装是一种将数据和操作封装在对象内部的机制。通过封装,对象的内部细节对外部是不可见的,只能通过对象的公共接口进行访问。

  4. 继承(Inheritance):继承是一种通过创建新类来扩展已有类的机制。子类(派生类)可以继承父类(基类)的属性和行为,并可以添加自己的特定属性和行为。

  5. 多态(Polymorphism):多态是一种允许使用统一的接口处理不同类型对象的能力。可以通过多态来实现方法的重写、方法的重载以及使用抽象类和接口。

面向对象编程的优点包括代码重用性、可维护性、扩展性和模块化等。它提供了一种更接近现实世界的建模方式,使得程序设计更加灵活、可靠且易于理解。

5、抽象类和接口

抽象类(Abstract Class)

  1. 定义:抽象类是一种不能被实例化的类,只能被其他类继承。抽象类可以包含抽象方法和非抽象方法。抽象方法是没有具体实现的方法,它只有声明没有方法体。

  2. 用途:抽象类通常用作其他类的基类,提供一些通用的功能,同时留下一些方法让子类具体实现。

  3. 特点

    • 可以包含成员变量,构造方法,和具体方法(有实现的方法)。
    • 可以有访问修饰符,如public、protected和private。
    • 子类通过继承抽象类并实现所有抽象方法,可以变为具体的类。

接口(Interface)

  1. 定义:接口是一种特殊的抽象类型,是方法声明的集合。它完全抽象,意味着它不能含有任何方法实现(Java 8之前)。从Java 8开始,接口可以包含静态方法和默认方法。

  2. 用途:接口主要用于定义对象的行为。类通过实现接口来保证提供接口声明的行为(方法)。

  3. 特点

    • 所有方法默认都是抽象的,直到Java 7为止。从Java 8开始,接口可以包含默认方法和静态方法。
    • 接口不能包含成员变量,但可以包含常量(public static final)。
    • 一个类可以实现多个接口,从而达到多重继承的效果。

抽象类是为了复用代码,接口是为了定义规范。

6、Java中有哪些类

普通类可以继承抽象类,实现抽象类的抽象方法,也可以实现接口,实现接口中生命的方法,两者有什么区别呢?什么时候该用抽象类,什么时候该用接口呢?

两者的区别就是使用场景的不同,虽然都要重写代码,因为是在不同的场景,所以起到了不同的作用。

使用抽象类的场景:共享代码与通用状态

使用接口的场景:定义行为协议

普通类:普通类是最常见的类类型,它们可以包含字段(变量)、方法、构造器等。普通类可以被实例化,即可以创建其对象。

抽象类:抽象类是不能被实例化的类,用来表达一个概念或者一个基本的设计。它们通常包含一个或多个抽象方法,即没有具体实现的方法。抽象类需要被其他类继承,并实现其所有的抽象方法。

最终类:最终类使用final关键字声明,这意味着它们不能被其他类继承。Java中的String类就是一个最终类的例子。

接口实现类:虽然接口本身不是类,但实现接口的类是一种特殊的类。这些类必须实现接口中声明的所有方法,除非它们自身被声明为抽象类。

枚举类:枚举类是一种特殊的类,用来定义变量的固定集合。枚举类是使用enum关键字定义的。枚举类在Java中是类型安全的,它们的每一个实例都是枚举类的一个唯一实例。

内部类:内部类是定义在其他类内部的类,他可以访问外部类的属性和方法、也可以实现某些特定的功能,内部类包括:

  • 成员内部类(非静态内部类):作为外部类的一个实例的一部分。
  • 静态内部类:作为外部类的一个静态成分,不需要外部类的实例。
  • 局部内部类:定义在方法中的类。
  • 匿名内部类:没有名字的一次性使用的内部类。

(有时间可以补充代码详细展示内部类)

7、方法重载

要实现方法重载,需要遵循以下几个基本规则:

  1. 相同的方法名:重载的方法必须使用相同的方法名。
  2. 不同的参数列表
    • 参数的数目不同;
    • 参数的类型不同;
    • 参数的顺序不同(如果参数的类型不相同)。
  3. 与返回类型无关:重载的方法可以有不同的返回类型,但重载解析通常不会考虑返回类型,所以仅改变返回类型不足以构成重载。
  4. 与访问修饰符无关:方法的访问修饰符可以不同,并不影响重载的有效性。

为什么返回值不同不算方法重载的原因有两个:

1、从程序的执行层面来讲:返回值不同如果作为方法重载,那么会产生歧义;

2、从JVM 方法签名的角度来讲:返回值并不属于方法签名的一部分,因此无法定位到具体的调用方法。

什么是方法签名?
方法签名(Method Siqnature)指的是方法的唯一标识,包括方法的名称、参数列表和参数的顺序。方法签名用于区分不同的方法,以便编译器和虚拟机能够正确地识别和调用特定的方法。如果两个方法具有相同的签名,则它们不能同时存在于同一个类中。方法签名与函数的返回类型或访问修饰符无关

 8、方法重载和方法重写:

方法重载(Overloading)

方法重载发生在同一个类中或者在一个类的继承体系中。它允许一个类有多个同名方法,但这些方法的参数列表必须不同。

特点

  • 同一个类中存在同名的方法。
  • 参数数量、类型或者顺序至少有一项不同。
  • 重载与方法的返回类型无关。
  • 重载是静态的,编译器在编译时就根据调用的方法签名确定使用哪个方法。

方法重写(Overriding)

方法重写发生在两个类的继承关系中,当子类定义一个与父类在方法签名上完全相同的方法时(包括方法名和参数列表)。

特点

  • 发生在父子类关系中。
  • 子类的方法与父类的方法具有相同的方法名、返回类型和参数列表。
  • 访问权限不能比父类中被重写的方法更严格。
  • 重写是动态的,具体调用哪个方法取决于对象的运行时类型。
class Animal {
    public void sound() {
        System.out.println("Animal sound");
    }
}
class Dog extends Animal {
    // 方法重写
    @Override
    public void sound() {
        System.out.println("Dog sound");
    }
    // 方法重载
    public void sound(int count) {
        for (int i = 0; i < count; i++) {
            System.out.println("Dog sound:" + i);
        }
    }
}

9、静态绑定&动态绑定:

在Java中,静态绑定和动态绑定是两种不同的方法调用机制,它们决定了程序运行时如何选择应调用的方法。这些机制是Java支持多态性的关键部分。

静态绑定(Static Binding):

静态绑定,又称为早期绑定,是在编译时进行的方法绑定。编译器已经确定了对象的方法调用,这通常适用于以下情况:

  • 静态方法:静态方法的调用在编译时就确定了,因为它们不依赖于任何对象的实例。
  • 私有方法:私有方法不可能被覆盖,因此它们总是在编译时绑定。
  • final方法:final方法不能被子类覆盖,所以它们的调用也是在编译时解析的。
  • 构造器:构造器调用也是静态绑定的,因为构造器不能被继承或覆盖。

静态绑定主要依赖于类型信息,而不是运行时的对象实例。

示例代码

public class BindingTest {
    private static void print() {
        System.out.println("Static method.");
    }

    private void show() {
        System.out.println("Private method.");
    }

    public static void main(String[] args) {
        BindingTest test = new BindingTest();
        test.show();  // 静态绑定
        print();      // 静态绑定
    }
}

动态绑定(Dynamic Binding):

动态绑定,又称为晚期绑定,是在运行时进行的方法绑定。如果一个方法是非私有的、非静态的,并且不是final的,则Java在运行时根据对象的实际类型来决定调用哪个方法,这实现了多态。

  • 实例方法:通常情况下,对象的实例方法使用动态绑定。Java虚拟机(JVM)在运行时查看对象的实际类型,并调用适当的方法。

示例代码

class Animal {
    void eat() {
        System.out.println("Animal is eating");
    }
}

class Dog extends Animal {
    @Override
    void eat() {
        System.out.println("Dog is eating");
    }
}

public class TestPolymorphism {
    public static void main(String[] args) {
        Animal myAnimal = new Dog();
        myAnimal.eat();  // 动态绑定
    }
}

在这个示例中,尽管myAnimal的引用类型是Animal,但是实际对象是Dog,所以eat方法的调用是在运行时解析的,Dog类中的eat方法被调用。

区分静态和动态绑定的关键:

  • 编译时与运行时:静态绑定发生在编译时,而动态绑定发生在运行时。
  • 方法类型:静态方法、私有方法、final方法和构造器涉及静态绑定,而普通的实例方法涉及动态绑定,前提是它们可以被覆盖。
  • 多态:动态绑定是实现多态性的关键机制。

通过这些机制,Java能够在运行时动态地调用正确的方法,使得程序设计更加灵活和强大。

10、静态&动态:

学习编程的过程中经常会遇见这两个字,但是我还没搞懂。

11、final & finally & finalize

12、String底层实现:

// Java8
private final char value[];

// Java17
@Stable
private final byte[] value;

String 底层是基于数组实现的,并且数组使用了final 修饰,不同版本中的数组类型也是不同的

  • JDK9之前(不含 JDK 9)String 类是使用 char[](字符数组)实现的。
  • JDK9之后,String 使用的是 byte[](字节数组)实现的。

PS:1个字符(char)=2个字节(byte)。

字符串常量池:

public class StringDemo {
    public static void main(String[] args) {
        String s1 = "aaa"; // “aaa” 被存储在字符串常量池中
        String s2 = "aaa"; // 如果字符串常量池中有“aaa”,将s2直接只想“aaa”;如果没有就在字符串常量池中先创建“aaa”
        System.out.println(s1==s2); // 结果为true,说明s1,s2这两个引用指向了同一个对象

        String s5 = new String(s1); // 凡是new()操作都会在堆中为对象开辟空间
        System.out.println(s5.equals(s1)); // 结果为true,因为两个字符串值相同
        System.out.println(s1==s5); // 结果为false,一个在字符串常量池,一个在堆区

        System.out.println(s5.hashCode());
        s5 = s1 + s2; // 本质上也是new()操作
        System.out.println(s5.hashCode());
        // 前后s5.hashCode()不同,说明s5前后指向了不同的对象,应证了(s1+s2)实际上是new()了一个新对象。
        // s5 是 对象引用 存储的是对象的内存地址
        // 对象是内存中的一块区域,用于存储数据和执行操作

    }
}

 13、String & StringBuffer & StringBuilder

在Java中,StringStringBufferStringBuilder都是用来处理字符串的,但它们在实现和使用场景上有明显的不同。理解这些差异可以帮助你更有效地在应用程序中管理和操作字符串。

1. String

String 类在Java中代表不可变的字符序列。这意味着一旦一个String对象被创建,它所包含的字符序列就不能更改。

特点

  • 不可变性:任何修改字符串内容的操作实际上都会创建一个新的String对象。
  • 线程安全String对象的不可变性使得它天然地线程安全,因为数据不能被更改。

用途

  • 适用于字符串不频繁改动的情景。
  • 安全关键的应用,如网络通信中字符串处理。

2. StringBuffer

StringBuffer 类提供了一个可变的字符串。与String不同,StringBuffer中的字符可以被修改。

特点

  • 可变性:可以在不创建新对象的情况下修改StringBuffer对象中的字符。
  • 线程安全StringBuffer的大多数公共方法都是同步的,这意味着它是线程安全的,但这也导致性能上的开销。

用途

  • 适用于需要改变字符串内容的应用场景,尤其是涉及多线程的情况,其中字符串经常变动。

3. StringBuilder

StringBuilder 类也提供了一个可变的字符串,但它在API和功能上与StringBuffer非常相似。主要的区别在于StringBuilder的方法不是同步的。

特点

  • 可变性:和StringBuffer一样,StringBuilder允许修改字符串内容而无需创建新的对象。
  • 非线程安全StringBuilder没有同步方法,这使得它在单线程环境下比StringBuffer更快。

用途

  • 适合在单线程环境下进行字符串操作,需要高效地修改字符串内容。

总结比较:

特性/类别 String StringBuffer StringBuilder
可变性 不可变 可变 可变
线程安全性 线程安全 线程安全 非线程安全
性能 高(不变更时) 中(同步开销) 高(无同步开销)
使用场景 不需修改的文本 多线程中需修改的文本 单线程中需修改的文本

选择建议:

  • 当你需要经常更改字符串内容的时候,优先考虑使用StringBuilder(如果是在单线程环境中)或StringBuffer(如果是在多线程环境中)。
  • 对于不需要修改的字符串,应使用String,特别是在字符串常量和字符串池可以带来优化的场景中。

通过这些细节和比较,你可以更好地理解何时使用每种类型的字符串处理类,从而在实际编程中做出合适的选择。

14、intern()方法

intern()方法用于将字符串添加到字符串常量池中,并返回常量池中对应的字符串对象的引用。
PS:常量池是 Java 运行时环境中的一个特殊存储区域,用于存储在编译期间确定的字符串常量和符号引用。当调用 String 的 intern()方法时,如果常量池中已经存在相同值的字符串,则返回对应的引用;如果常量池中不存在,则在常量池中创建并返回对应的引用。

public class InternExample {
    public static void main(String[] args) {
        String s1 = new String("hello");
        String s2 = new String("hello");
        String s3 = "hello";

        System.out.println(s1 == s2); // 输出 false
        System.out.println(s1 == s3); // 输出 false
        System.out.println(s2 == s3); // 输出 false

        String s1Interned = s1.intern();
        String s2Interned = s2.intern();

        System.out.println(s1Interned == s2Interned); // 输出 true
        System.out.println(s3 == s1Interned); // 输出 true
    }
}

使用场景

  • 优化内存使用:在处理大量字符串且字符串重复率高的场景下,使用 intern() 可以显著减少内存占用。
  • 提高性能:对于字符串密集的操作,通过减少重复字符串的内存占用,可以提高性能。

注意事项

虽然 intern() 可以节省内存,但是它也可能导致性能问题,特别是当常量池过大时,因为维护常量池需要时间和资源。此外,对于短生命周期的字符串使用 intern() 可能是不划算的,因为这可能导致长生命周期的内存占用。因此,在决定是否使用 intern() 时,应该根据具体情况进行权衡。

15、包装类

  1. 面向对象要求:Java 是一门面向对象的编程语言,要求所有的数据都应该是对象。但是,基本数据类型(如int、char、double等)并不是对象,它们没有成员方法和其他面向对象的特性。为了满足面向对象编程的要求,Java 引入了包装类,将基本数据类型封装成对象,使得它们也具有面向对象的特性。例如集合的操作只能是对象,而不能为基础数据类型。
  2. 提供了更多的功能和方法:包装类提供了一些额外的方法和功能,例如执行数学运算、比较大小、转换数据类2型 Integer.valueOf(n)等方法。
  3. 泛型要求:泛型(Generics)是 Java 中很重要的特性,它提供了类型安全和代码重用的功能。但是,泛型要求3类型参数必须是对象类型,不能是基本数据类型。因此,如果想在泛型中使用基本数据类型,就需要使用对应的包装类。
  4. 表示 null 值:包装类可以表示 null 值,而基本数据类型不能。这在某些场景下很有用,比如在接口传参中如果使用包装类即使前端不传参也不会报错,而使用基本数据类型,如果前端忘记传参就会报错。

包装类常用的场景有:

  1. 用于泛型数据存储
  2. 用于集合类数据存储
  3. 方法的参数传递

装箱&拆箱:

  • 装箱:基本数据类型自动转化为对应的包装类。
  • 拆箱:包装类自动转化为对应的基本数据类型。

需要注意的是,装箱和拆箱的过程可能涉及到对象的创建和销毁,可能会引入额外的开销和性能损耗。因此,在性能要求较高的场景中,应尽量避免频繁的装箱和拆箱操作,以减少额外的开销。 // 这就看实际的工作场景了,或者性能优化时可以考虑

包装类缓存:

在Java中,包装类缓存主要涉及到整数和字符的包装类,如IntegerLongShortByteCharacter等。这些类通过缓存常用的对象来减少内存使用和提高性能。缓存机制使得在特定的范围内的包装对象不需要重复创建,相同值的对象可以共享。

Integer a = 10;// 缓存范围内的整数,从缓存中获取
Integer b= 10;// 缓存范围内的整数,从缓存中获取
Integer c=128;//超出缓存范围,创建新的对象
Integer d=128;// 超出缓存范围,创建新的对象
System.out.println("a==b:"+(a == b));//输出:a ==b:true,因为a和b引用的是缓存中同一个
System.out.println("c== d:"+(c== d));//输出:c==d:false,因为c和d是两个独立的对象

// 什么鬼东西啊,用得到吗,简单了解一下吧先

除了包装类,还有原子类呢。

16、擦除机制

在Java中,擦除机制(Type Erasure)是泛型(Generics)实现的一个关键特性,它允许在编译时检查类型安全,同时保证了泛型代码与Java早期版本的兼容性。这一机制涉及到编译器在编译过程中将泛型类型参数替换掉,并可能添加类型转换的代码,以确保应用在运行时的安全性。

擦除机制的工作原理

当代码被编译时,Java编译器将泛型类型参数移除(擦除),将其替换为非泛型的代码。这个过程主要包括:

  1. 替换类型参数:泛型类型参数被替换为第一个边界的类型(如果有的话),或者如果没有指定边界,则替换为`Object`。例如,一个泛型类 `Box<T>` 中的 `T` 会被替换为 `Object`。
  2. 类型转换的插入:在必要的地方插入类型转换代码,以确保类型安全。
  3. 桥接方法的生成:为了保持泛型类或接口中的多态性,并确保擦除后的类在继承关系中的方法能够正确覆盖或实现,编译器可能会生成桥接方法(Bridge Methods)。

示例:泛型类
考虑以下泛型类定义和使用:

public class Box<T> {
    private T t;

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }
}

Box<Integer> intBox = new Box<>();
intBox.set(123);
Integer num = intBox.get();

在编译后,`Box<T>` 类的代码大致会被转换为如下形式(假设没有指定边界):

public class Box {
    private Object t; // 将泛型擦除成Object或者其上界

    public void set(Object t) {
        this.t = t;
    }

    public Object get() {
        return t;
    }
}
Box<Integer> intBox = new Box();
intBox.set(123);
Integer num = (Integer)intBox.get(); // 编译器自动插入类型转化代码

在使用时,如 `intBox.set(123)` 和 `Integer num = intBox.get()` 的地方,编译器会插入适当的类型转换。

// build前后:我也不知道build或做哪些工作,相当于把代码优化一下吧,注释没有了,还会插入一些代码,比如类型转化,无参构造。target里面的是

// .java文件字节码文件

示例:泛型接口

build后的.class文件和字节码文件:

可以看出多了一个桥接方法用于类型转化。

// 自动生成的桥接方法,调用我们重写的方法
public int fun(Object o) {
    return fun((Student) o);
}

// 重写接口中的方法
public int fun(Student o) {
    return 0;
}

擦除机制的好处

  1. 兼容性:擦除机制确保了新编写的泛型代码能够与早期Java版本的代码无缝工作。既有库和应用程序无需改动即可继续使用,因为泛型信息只在编译阶段存在,运行时的字节码中不包含泛型类型信息。
  2. 简化泛型的实现:通过擦除机制,Java的泛型实现避免了对JVM进行大规模修改。泛型在语言层面上得到了支持,而虚拟机则继续像处理普通类和接口一样处理擦除后的泛型。
  3. 避免运行时开销:因为泛型类型信息在编译后不存在,运行时不需要进行额外的类型检查。这减少了运行时的开销,尽管在某些情况下需要进行类型转换。

擦除机制的缺点 (有点抽象,没看懂)

尽管擦除机制带来了好处,它也有一些限制,比如无法在运行时查询泛型类型信息(因为泛型信息不保留到运行时),这限制了某些反射操作的能力。此外,它可能导致编写某些泛型代码时需要额外的类型转换或方法,这些方法在具体使用中可能会造成混淆。

如何理解泛型擦除机制的兼容性-CSDN博客

17、值传递&引用传递

什么是值传递,什么是引用传递。为什么说Java中只有值传递。_为什么引用传递-CSDN博客

C++中的值传递和引用传递:

Java值传递:

基本数据类型只能值传递不能引用传递,就比如swap(int a,int b)只是两个形参进行交换,并不会影响是实参的值。C++中的swap(int *a,int *b)就可以实现两个实参的交换。

对于应用类型,函数栈区中对象引用存储了对象的地址,在传递时,会将这个对象的地址复制形参,这样以来,就可以通过形参修改对象。需要注意的值,如果在方法内部重新new()了一个对象给形参,那么此时形参和实参指向的就是两个对象。

package com.example.demo.JavaBasics;

class Person {
    public String name;
    public int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class demo_4_20 {

    public static void main(String[] args) {
        Person actual = new Person("zxx", 21);
        System.out.println("修改前:" + actual.toString());
        fun(actual); // actual实参,引用类型数据
        System.out.println("修改后:" + actual.toString());

    }

    public static void fun(Person formal) { // formal形参,和实参actual指向同一个对象
        formal.name = "hyf";// 因为formal和actual指向同一个对象,所以修改formal,actual也会改变
        formal = new Person("zw", 22); // 创建了一个新的对象,formal和actual不在指向同一个对象,此时修改形参不会影响实参
        // 上面是显式地创建对象,有时候会隐式地创建对象:
        String s = new String("abc");
        System.out.println("s1:"+System.identityHashCode(s));
        s = "abc"; // 隐式地创建了新对象
        System.out.println("s2:"+System.identityHashCode(s));
        Integer i = new Integer(10);
        System.out.println("i1:"+System.identityHashCode(i));
        i = 20; // 隐式地创建了新对象
        System.out.println("i2:"+System.identityHashCode(i));
        // identityHashCode的值前后不同,说明隐式创建了新的对象
    }
}
修改前:Person{name='zxx', age=21}
s1:168423058
s2:821270929
i11160460865
i2:1247233941
修改后:Person{name='hyf', age=21}

Process finished with exit code 0

搞清楚几个概念就可以了:

对象引用、对象引用的地址、对象应引用的值(即对象引用的地址里面装的是什么)

对象、对象的地址、对象的值(即对象的地址里面装的是什么)

对象引用的值就是对象的地址

值传递:创建一个临时变量,复制实参的值给临时变量

应用传递:创建一个临时变量,将实参的地址赋值给临时变量

Java中函数传参时,无论时基础数据类型还是引用数据类型都是值传递。

18、Exception & Error

// 受查异常,运行时异常,乱七八招的,不仅牵扯到异常的分类,还跟编译,虚拟机有关系

// 异常和错误两个事情

在Java中,异常(Exception)是程序运行时可能发生的错误或意外情况的表示。Java的异常分为两种类型:受检异常(Checked Exception)和非受检异常(Unchecked Exception)。

  1. 受检异常(Checked Exception)

    • 受检异常是指在编译时由编译器强制检查的异常,必须在代码中进行处理,否则会导致编译错误。
    • 通常是继承自Exception类的异常,除了RuntimeException及其子类以外的所有异常都是受检异常。
    • 例如,IOExceptionSQLException等都是受检异常,必须使用try-catch块或者在方法签名中使用throws关键字声明抛出异常。
  2. 非受检异常(Unchecked Exception)

    • 非受检异常是指在运行时可能发生的异常,不需要在代码中显式处理,但可以选择处理。
    • 通常是继承自RuntimeException类的异常,以及Error及其子类的异常。
    • 例如,NullPointerExceptionArrayIndexOutOfBoundsException等都是非受检异常。

Exception 和 Error 都是继承了 Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch)它是异常处理机制的基本组成类型。

  1. 级别不同:Exception 是表示可恢复的异常情况,而 Error 表示不可恢复的严重错误。
  2. 来源不同:Exception 通常由应用程序代码引起,表示可预料的异常情况,如输入错误、文件不存在等。而2.Error 通常由 Java 虚拟机 (JVM)引起,表示严重的系统层面的错误(如内存溢出、栈溢出等),通常无法通过代码来处理。
  3. 代码处理不同:Exception 通常需要程序员在代码中明确地捕获并处理,以防止应用程序的崩溃或异常终止。3而 Error 通常是无法通过代码处理的,它表示系统出现了严重的问题,无法恢复。
  4. 程序影响不同:Exception 是一种正常的控制流程,可能会影响应用程序的正常执行,但不会导致应用程序终4.止。而 Error 是一种严重的问题,可能会导致应用程序的崩溃或终止。

总的来说,Exception 表示可以通过代码处理的可恢复的异常情况,通常由应用程序引起;而 Error 表示不可恢复的严重错误,通常由 Java 虚拟机(JVM)引起,无法通过代码处理。

19、什么是反射,为什么需要反射

// 只是会用,理解起来还挺麻烦的(有空再看吧,先找个八股文背一背)

Java反射技术在以下情况下可能会派上用场:

  1. 框架和库的设计:许多框架和库(如Spring框架)利用反射来实现依赖注入、AOP(面向切面编程)等功能。这些功能需要在运行时动态地创建对象、调用方法、访问属性等。

  2. 配置文件解析:有时候需要根据配置文件中的类名或方法名来动态地加载类和调用方法。反射技术可以在运行时根据配置信息来实现这些操作。

  3. 插件系统:插件系统需要在运行时动态加载和卸载插件,并调用插件中定义的方法。反射技术可以用于实现插件的动态加载、调用和卸载。

  4. 序列化和反序列化:在序列化和反序列化过程中,可能需要根据类的结构动态地创建对象,并访问对象的属性。反射技术可以用于实现序列化和反序列化过程中的对象创建和属性访问。

  5. 单元测试:在单元测试中,有时候需要调用类的私有方法或访问私有属性以进行测试。反射技术可以在测试代码中实现这些操作。

  6. 动态代理:动态代理是一种常见的设计模式,用于在运行时创建代理对象来控制对另一个对象的访问。反射技术可以用于实现动态代理。

  7. 动态加载类:有时候需要根据条件动态加载类,并调用类中的方法。反射技术可以用于实现动态加载类和调用方法。

总的来说,Java反射技术提供了一种在运行时动态地检查和修改类、对象、方法和属性的能力,使得程序能够更加灵活和可扩展。然而,反射技术也会增加代码的复杂性和运行时开销,因此在普通应用程序中应谨慎使用。

// 还是找视频好好看看Java反射机制吧

package com.example.demo.JavaBasics;

import java.lang.annotation.Annotation;
import java.lang.reflect.*;

public class ReflectionExample {
    public static void main(String[] args) throws Exception {
        // 获取类对象
        Class<?> clazz = MyClass.class;

        // 获取构造函数并创建对象
        Constructor<?> constructor = clazz.getConstructor();
        Object obj = constructor.newInstance();

        // 获取方法并调用
        Method method = clazz.getMethod("sayHello", String.class);
        method.invoke(obj, "Alice");

        // 获取字段并访问
        Field field = clazz.getField("publicField");
        System.out.println("Public Field Value: " + field.get(obj));

        Field privateField = clazz.getDeclaredField("privateField");
        privateField.setAccessible(true);
        System.out.println("Private Field Value: " + privateField.get(obj));

        // 获取注解
        Annotation annotation = clazz.getAnnotation(MyAnnotation.class);
        System.out.println("Annotation Value: " + ((MyAnnotation) annotation).value());

        // 动态代理
        InvocationHandler handler = new MyInvocationHandler();
        Class<?>[] interfaces = { MyInterface.class };
        Object proxy = Proxy.newProxyInstance(clazz.getClassLoader(), interfaces, handler);
        ((MyInterface) proxy).doSomething();
    }
}

class MyClass {
    public String publicField = "Public Field";
    private String privateField = "Private Field";

    @MyAnnotation("Hello Annotation")
    public void sayHello(String name) {
        System.out.println("Hello, " + name + "!");
    }
}

interface MyInterface {
    void doSomething();
}

class MyInvocationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Dynamic Proxy: Before method invocation");
        Object result = method.invoke(proxy, args);
        System.out.println("Dynamic Proxy: After method invocation");
        return result;
    }
}

@interface MyAnnotation {
    String value();
}

 20、为什么反射的执行比较慢

// 如果能投理解透彻,为什么可以根据类名就可以获取这个类,那么就离理解反射为什么这么慢不远了。

21、动态代理 & 静态代理

// 太高深了,结合具体的应用场景

22、序列化 (protobuf)

序列化是指将对象转换为字节序列的过程,以便在网络传输、持久化存储或跨平台数据交换时使用。反之,将字节序列转换回对象的过程称为反序列化。

在Java中,序列化是通过实现`Serializable`接口来实现的。当一个类实现了`Serializable`接口,它的对象就可以被序列化和反序列化。

为什么要序列化呢?以下是一些常见的原因:

  1. 数据持久化:通过序列化,可以将对象的状态保存到磁盘或数据库中,以便在程序重新启动后恢复对象的状态。这对于实现数据持久化和持久层的功能非常有用。
  2. 远程通信:在分布式系统中,可以通过序列化将对象转换为字节序列,在网络上传输。这使得在不同的机器之间传递对象变得更加方便。常见的应用场景包括远程方法调用(RMI)、Web服务、消息队列等。
  3. 对象复制:序列化可以用于创建对象的深拷贝。通过将对象序列化为字节序列,然后再进行反序列化,可以得到原始对象的一个完全独立的副本。
  4. 缓存和共享:序列化可以用于缓存和共享对象。通过将对象序列化并存储在缓存系统中,可以避免频繁地重新创建对象,提高性能。多个应用程序或服务可以共享序列化的对象,以实现数据共享和协作。

需要注意的是,在进行序列化时,可能会遇到以下情况:

  • 有些类可能不可序列化,比如含有不可序列化字段的类或某些特定的系统类。
  • 序列化的效率相对较低,尤其是对于大对象和复杂对象图。
  • 序列化和反序列化的过程可能会引入版本兼容性问题,特别是在不同的Java版本或不同的平台上。

因此,在进行序列化时,需要考虑对象的可序列化性、性能和版本兼容性等因素。同时,也可以通过一些优化策略(如选择合适的序列化方式、压缩数据等)来改善序列化的效率和性能。

import java.io.*;

class MyClass implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    public MyClass(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

public class SerializationExample {
    public static void main(String[] args) {
        // 创建对象
        MyClass obj = new MyClass("Alice", 25);

        // 序列化对象到文件
        String fileName = "object.ser";
        serializeObject(obj, fileName);

        // 从文件反序列化对象
        MyClass deserializedObj = deserializeObject(fileName);

        // 打印反序列化后的对象属性
        if (deserializedObj != null) {
            System.out.println("Name: " + deserializedObj.getName());
            System.out.println("Age: " + deserializedObj.getAge());
        }
    }

    // 序列化对象到文件
    private static void serializeObject(MyClass obj, String fileName) {
        try (FileOutputStream fos = new FileOutputStream(fileName);
             ObjectOutputStream oos = new ObjectOutputStream(fos)) {
            oos.writeObject(obj);
            System.out.println("Object serialized successfully.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 从文件反序列化对象
    private static MyClass deserializeObject(String fileName) {
        MyClass obj = null;
        try (FileInputStream fis = new FileInputStream(fileName);
             ObjectInputStream ois = new ObjectInputStream(fis)) {
            obj = (MyClass) ois.readObject();
            System.out.println("Object deserialized successfully.");
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return obj;
    }
}
import java.io.*;

class MyClass implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    public MyClass(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

public class SerializationExample {
    public static void main(String[] args) {
        // 创建对象
        MyClass obj = new MyClass("Alice", 25);

        // 序列化对象到字节数组
        byte[] serializedData = serializeObject(obj);

        // 从字节数组反序列化对象
        MyClass deserializedObj = deserializeObject(serializedData);

        // 打印反序列化后的对象属性
        if (deserializedObj != null) {
            System.out.println("Name: " + deserializedObj.getName());
            System.out.println("Age: " + deserializedObj.getAge());
        }
    }

    // 序列化对象到字节数组
    private static byte[] serializeObject(MyClass obj) {
        try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
             ObjectOutputStream oos = new ObjectOutputStream(bos)) {
            oos.writeObject(obj);
            return bos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    // 从字节数组反序列化对象
    private static MyClass deserializeObject(byte[] data) {
        try (ByteArrayInputStream bis = new ByteArrayInputStream(data);
             ObjectInputStream ois = new ObjectInputStream(bis)) {
            return (MyClass) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}

serialVersionUID是Java序列化机制中的一个静态变量,它用于标识序列化类的版本。它是一个长整型数值。

在进行对象的序列化和反序列化时,Java会根据serialVersionUID来判断序列化类和反序列化类是否兼容。如果两个类的serialVersionUID值相同,Java将认为它们是同一版本的类,可以进行序列化和反序列化操作。如果serialVersionUID值不同,则会抛出InvalidClassException,表示类的版本不兼容,无法进行正常的序列化和反序列化。

serialVersionUID的作用是提供一种机制,以确保在类的版本发生变化时,仍然能够正确地进行序列化和反序列化操作。如果对类的定义做了修改(例如添加、删除或修改了成员变量、方法等),可以通过显式地指定serialVersionUID来控制类的版本号,以确保兼容性。

如果不显式地指定serialVersionUID,Java编译器会根据类的结构自动生成一个默认的serialVersionUID。但是,建议在进行序列化时显式地定义serialVersionUID,以便更好地控制类的版本。例如,当类的成员变量发生变化时,可以手动修改serialVersionUID,以防止反序列化时出现不兼容的情况。

要注意的是,serialVersionUID的值是根据类的结构(包括成员变量、方法等)计算得出的,因此,如果对类的结构进行了修改,serialVersionUID的值也会改变。

对于一个单例对象,将其序列化然后再反序列化可能会得到一个新的单例对象

23、深克隆 & 浅克隆

深克隆(Deep Clone)和浅克隆(Shallow Clone)是对象复制的两种不同方式。

浅克隆是指创建一个新对象,并将原始对象中的字段值复制到新对象中。如果字段是基本数据类型,它们的值会被直接复制;如果字段是引用类型,那么新对象中的字段将引用原始对象中相同的对象。换句话说,浅克隆只复制对象的引用,而不复制引用的对象本身。

深克隆是指创建一个新对象,并将原始对象中的字段值复制到新对象中。不同于浅克隆,深克隆会递归地复制对象的所有引用类型字段,确保复制的对象与原始对象完全独立,即两个对象之间没有共享的引用。这样,对其中一个对象所做的修改不会影响另一个对象。

Java提供了Cloneable接口和clone()方法来实现对象的浅克隆。然而,这种默认情况下的浅克隆只能复制对象的字段值,并不会复制引用对象。如果需要进行深克隆,需要在对象的类中重写clone()方法,并在该方法中递归复制引用类型字段。

以下是一个示例代码,演示了浅克隆和深克隆的区别:

package com.example.demo.JavaBasics;
import java.lang.Cloneable;

public class CopyExample {
    public static void main(String[] args) throws CloneNotSupportedException {
        //克隆地址,没有引用类型
        Address address1 = new Address("安徽","亳州");
        Address address2 = (Address) address1.clone();//抛异常、类型转化
        System.out.println(address2.province+address2.city);
        // 浅克隆
        Staff s1 = new Staff("lisa",21,address1);
        Staff s2 = (Staff) s1.clone(); //抛异常、类型转化
        System.out.println(s1.address == s2.address);//没有重写clone方法,浅拷贝,结果为true

        // 深克隆
        Staff s3 = (Staff) s1.clone();//抛异常、类型转化
        System.out.println(s1.address==s3.address);//重写了clone方法,深拷贝,结果为false
    }
}
class Staff implements Cloneable{ //Cloneable接口里面一个方法也没有啊,重写的Object的clone方法
    public String name;
    public int age;
    public Address address;

    public Staff(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        // 方式一:
        Staff staff1 = new Staff(name,age, (Address) address.clone());
        // 方式二:
        Staff staff2 = (Staff) super.clone();
        staff2.address = (Address) this.address.clone();
        return staff2;
    }
}
class Address implements Cloneable {
    public String province;
    public String city;

    public Address(String province, String city) {
        this.province = province;
        this.city = city;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

}

在深克隆中,通过递归复制引用类型字段,确保了克隆对象与原始对象之间的字段值独立性。

// 同序列化反序列化也可以实现深克隆

24、BIO & NIO & AIO

Docs

以下是基于Java的简单示例代码,演示了BIO、NIO和AIO的用法。

1. BIO 示例:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class BioExample {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("BIO server started on port 8080");

        while (true) {
            Socket socket = serverSocket.accept();
            System.out.println("Accepted connection from client: " + socket.getInetAddress());

            // 处理客户端请求
            new Thread(() -> {
                try {
                    InputStream inputStream = socket.getInputStream();
                    OutputStream outputStream = socket.getOutputStream();

                    byte[] buffer = new byte[1024];
                    int bytesRead;
                    while ((bytesRead = inputStream.read(buffer)) != -1) {
                        String request = new String(buffer, 0, bytesRead);
                        System.out.println("Received request from client: " + request);

                        // 做一些处理
                        String response = "Hello, client!\n";
                        outputStream.write(response.getBytes());
                        outputStream.flush();
                    }

                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

在上述代码中,使用`ServerSocket`创建一个BIO服务器,监听8080端口。当有客户端连接时,会创建一个新的线程来处理客户端请求。在处理过程中,通过`InputStream`读取客户端发送的数据,并通过`OutputStream`发送响应给客户端。

2. NIO 示例:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class NioExample {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false);
        System.out.println("NIO server started on port 8080");

        ExecutorService executorService = Executors.newFixedThreadPool(10);

        while (true) {
            SocketChannel socketChannel = serverSocketChannel.accept();
            if (socketChannel != null) {
                System.out.println("Accepted connection from client: " + socketChannel.getRemoteAddress());

                // 处理客户端请求
                executorService.submit(() -> {
                    try {
                        ByteBuffer buffer = ByteBuffer.allocate(1024);

                        while (socketChannel.read(buffer) != -1) {
                            buffer.flip();

                            String request = new String(buffer.array(), 0, buffer.limit());
                            System.out.println("Received request from client: " + request);

                            // 做一些处理
                            String response = "Hello, client!\n";
                            buffer.clear();
                            buffer.put(response.getBytes());
                            buffer.flip();
                            socketChannel.write(buffer);
                            buffer.clear();
                        }

                        socketChannel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
            }
        }
    }
}

在上述代码中,使用`ServerSocketChannel`创建一个NIO服务器,监听8080端口。通过`configureBlocking(false)`设置为非阻塞模式。当有客户端连接时,会创建一个新的线程来处理客户端请求。在处理过程中,通过`SocketChannel`读取客户端发送的数据,并使用`ByteBuffer`进行数据的读写操作。

3. AIO 示例:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutionException;

public class AioExample {
    public static void main(String[] args) throws IOException, InterruptedException, ExecutionException {
        AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8080));
        System.out.println("AIO server started on port 8080");

        serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
            @Override
            public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
                try {
                    System.out.println("Accepted connection from client: " + socketChannel.getRemoteAddress());

                    ByteBuffer buffer = ByteBuffer.allocate(1024);

                    socketChannel.read(buffer, null, new CompletionHandler<Integer, Object>() {
                        @Override
                        public void completed(Integer bytesRead, Object attachment) {
                            if (bytesRead > 0) {
                                buffer.flip();

                                String request = new String(buffer.array(), 0, buffer.limit());
                                System.out.println("Received request from client: " + request);

                                // 做一些处理
                                String response = "Hello, client!\n";
                                buffer.clear();
                                buffer.put(response.getBytes());
                                buffer.flip();
                                socketChannel.write(buffer, null, new CompletionHandler<Integer, Object>() {
                                    @Override
                                    public void completed(Integer bytesWritten, Object attachment) {
                                        try {
                                            socketChannel.close();
                                        } catch (IOException e) {
                                            e.printStackTrace();
                                        }
                                    }

                                    @Override
                                    public void failed(Throwable exc, Object attachment) {
                                        exc.printStackTrace();
                                        try {
                                            socketChannel.close();
                                        } catch (IOException e) {
                                            e.printStackTrace();
                                        }
                                    }
                                });
                            }
                        }

                        @Override
                        public void failed(Throwable exc, Object attachment) {
                            exc.printStackTrace();
                            try {
                                socketChannel.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
                exc.printStackTrace();
            }
        });

        // 阻塞主线程,保持服务器运行
        Thread.currentThread().join();
    }
}

在上述代码中,使用`AsynchronousServerSocketChannel`创建一个AIO服务器,监听8080端口。当有客户端连接时,通过`accept`方法接收客户端连接,并在连接建立后,使用`read`方法读取客户端发送的数据。在读取完成后,进行一些处理,并使用`write`方法将响应数据写回客户端。

25、重写 equals 时为什么一定要重写 hashCode?

面试官:重写 equals 时为什么一定要重写 hashCode?-为什么重写equals必须重写hashcode


网站公告

今日签到

点亮在社区的每一天
去签到