Java基础面试题(四)

1. 深克隆和浅克隆的区别?

深克隆和浅克隆的主要区别在于它们处理对象中的引用类型字段的方式不同,这导致它们在复制对象时的行为有所不同。

浅克隆(Shallow Clone)在复制对象时,对于非基本类型(即引用类型)的属性,只复制其引用地址,而不复制引用的对象本身。这意味着,原始对象和克隆对象将共享这些引用类型的对象。因此,修改其中一个对象的引用类型属性会影响到另一个对象,因为它们实际上是指向内存中的同一个对象。在Java中,通过覆盖Object类的clone()方法可以实现浅克隆。

深克隆(Deep Clone)则不同,它不仅复制对象本身,还递归地复制对象中所有引用类型的属性,生成完全独立的对象副本。这样,原始对象和克隆对象将完全脱离,对其中任意一个对象的修改都不会影响到另一个对象。实现深克隆通常比浅克隆更复杂,可能需要手动编写代码来处理所有引用类型字段的复制,或者使用如序列化和反序列化的方式来实现。

总结来说,浅克隆和深克隆的主要区别在于它们对引用类型字段的处理方式不同:浅克隆复制引用地址,而深克隆复制引用的对象本身。这导致了它们在复制对象时的不同行为:浅克隆的对象之间会共享引用类型的对象,而深克隆则生成完全独立的对象副本。

2. 什么是 Java 的序列化,如何实现 Java 的序列化?

Java 的序列化是指将对象转换为字节流的过程,以便在网络上传输或保存到本地文件中。反序列化则是将字节流转换回对象的过程。通过序列化和反序列化,我们可以将对象的状态保存下来,并在需要时恢复对象的状态。

要实现 Java 的序列化,需要满足以下条件:

  1. 实现 Serializable 接口
    要序列化的类必须实现 java.io.Serializable 接口。这个接口是一个标记接口,没有定义任何方法。实现这个接口意味着该类可以被序列化。

  2. 定义 serialVersionUID
    建议为每个可序列化的类定义一个唯一的 serialVersionUID。这个 ID 用于在反序列化时验证发送者和接收者加载的类是否匹配。如果不定义,JVM 会根据类的详细信息自动生成一个,但这可能会导致在不同版本的类之间出现不兼容问题。

  3. 确保所有字段都可序列化
    如果类的字段是引用类型,并且该引用类型没有被标记为 transient,那么它也必须是可序列化的。否则,序列化过程会抛出 NotSerializableException 异常。如果某个字段不需要被序列化,可以使用 transient 关键字进行标记。

下面是一个简单的 Java 序列化示例:

import java.io.*;

public class Employee implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private transient int age; // 这个字段不会被序列化

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

    // getter 和 setter 方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public static void main(String[] args) {
        Employee emp = new Employee("Alice", 30);

        try {
            // 序列化对象到文件
            FileOutputStream fileOut = new FileOutputStream("employee.ser");
            ObjectOutputStream out = new ObjectOutputStream(fileOut);
            out.writeObject(emp);
            out.close();
            fileOut.close();

            // 从文件反序列化对象
            FileInputStream fileIn = new FileInputStream("employee.ser");
            ObjectInputStream in = new ObjectInputStream(fileIn);
            Employee emp2 = (Employee) in.readObject();
            in.close();
            fileIn.close();

            System.out.println("Deserialized Employee...");
            System.out.println("Name: " + emp2.getName());
            System.out.println("Age: " + emp2.getAge()); // 由于 age 是 transient,所以这里会输出 0
        } catch (IOException i) {
            i.printStackTrace();
            return;
        } catch (ClassNotFoundException c) {
            System.out.println("Employee class not found");
            c.printStackTrace();
            return;
        }
    }
}

示例中,我们创建了一个 Employee 类,并实现了 Serializable 接口。我们定义了一个 serialVersionUID,并将 age 字段标记为 transient,因此它不会被序列化。在 main 方法中,我们创建了一个 Employee 对象,并将其序列化到文件中。然后,我们从文件中反序列化对象,并打印出反序列化后的对象的状态。由于 age 字段是 transient 的,所以反序列化后的对象的 age 字段的值会是默认值(对于 int 类型,默认值是 0)。

3. 什么情况下需要序列化?

在Java中,序列化主要用于以下场景:

  1. 对象持久化:将对象的状态保存到硬盘上,使其可以在程序运行之外的时间存在。例如,一个程序可能在运行时创建了一些对象,当程序结束后,这些对象就消失了。但如果程序将对象序列化到硬盘上,那么在其他时间或者其他程序中,这些对象的状态可以被重新加载并继续使用。这常用于存储用户的配置信息、程序的状态等。

  2. 网络传输:在网络通信中,需要将对象从一个节点发送到另一个节点。由于网络只能传输字节流,因此需要将对象序列化为字节流后在网络上传输,然后在接收端进行反序列化以恢复对象。这常见于远程方法调用(RMI)、Web服务(如REST或SOAP)、分布式计算等场景。

  3. 复制对象:有时,你可能需要在内存中创建对象的深拷贝。虽然可以通过编写特定的拷贝构造函数或方法来实现这一点,但使用序列化/反序列化也可以实现相同的目的,特别是当对象结构复杂且包含嵌套对象时。

  4. 使用Java的某些功能:有些Java框架或库,如Hibernate(ORM框架),使用序列化来保存和恢复对象的状态。

  5. 缓存:在某些情况下,你可能希望将对象存储在缓存中以便快速访问。如果缓存系统支持对象存储,那么通常需要通过序列化将对象转换为字节流。

需要注意的是,不是所有的对象都需要或应该被序列化。只有那些确实需要被持久化、传输或复制的对象才应该实现Serializable接口。同时,当实现序列化时,需要确保对象的所有非瞬态字段都是可序列化的,否则在序列化时会抛出NotSerializableException异常。

此外,虽然序列化提供了一种方便的对象状态保存和恢复机制,但它也有一些缺点,如性能开销、版本兼容性问题等。因此,在决定是否使用序列化时,需要综合考虑其优缺点和具体需求。

4. Java 的泛型是如何工作的 ? 什么是类型擦除 ?

Java的泛型主要通过参数化类型来实现,即所操作的数据类型被指定为一个参数。泛型是一种特殊的类型,它把类型的明确工作推迟到创建对象或者调用方法的时候才去明确。泛型可以应用在方法、接口和类上,分别称为泛型方法、泛型接口和泛型类。

泛型的工作机制与类型擦除紧密相关。类型擦除是在编译期明确去掉所编程序的类型系统。也就是说,在编译时,编译器会擦除所有与类型相关的信息,因此在运行时并不存在任何类型相关的信息。这样做的目的是确保能和Java 5之前的版本开发的二进制类库进行兼容。

类型擦除的具体转换规则如下:

  • 如果泛型没有设置类型上限,那么将泛型转化成Object类型。
  • 如果设置了类型上限,那么将泛型转化成他的类型上限。

例如,List、List、List在类型擦除后都会变成List。这种转换使得在运行时无法访问到具体的类型参数,因为编译器已经把泛型类型转换成了原始类型。

然而,类型擦除也带来了一些问题。例如,如果两个泛型方法具有相同的名字和相同的参数类型(擦除后的类型),即使它们的泛型类型参数不同,也会引发编译错误。这是因为在运行时,这两个方法被视为具有相同的签名。

总的来说,Java的泛型通过类型参数化提高了代码的复用性和类型安全,而类型擦除则是实现这一机制的关键技术,同时也带来了一些需要特别注意的问题。

5. 什么是泛型中的限定通配符和非限定通配符 ?

在Java的泛型中,通配符(Wildcard)用于表示未知的类型。通配符主要有两种形式:限定通配符(Bounded Wildcard)和非限定通配符(Unbounded Wildcard)。

非限定通配符:非限定通配符用 表示,它表示未知的类型。使用非限定通配符的泛型类型可以被任何类型所替代。例如,List 表示一个元素类型为未知类型的列表。这个列表可以接受任何类型的元素,但是在没有额外信息的情况下,我们不能向这个列表添加元素(除了null),也不能获取列表元素的确切类型(只能获取到Object类型)。

限定通配符:限定通配符用于对泛型类型进行限制。它有两种形式:

  1. 上界限定通配符(Upper Bounded Wildcard):格式为 ,它表示类型必须是T类型或者是T的子类型。例如,List 表示一个元素类型为Number或者Number的子类(如Integer、Double等)的列表。这种形式的通配符在需要读取列表元素时特别有用,因为你可以确保读取的元素至少是T类型或其子类型。
  2. 下界限定通配符(Lower Bounded Wildcard):格式为 ,它表示类型必须是T类型或者是T的父类型。例如,List 表示一个元素类型为Integer或者是Integer的父类(如Number、Object等)的列表。这种形式的通配符在需要向列表添加元素时特别有用,因为你可以确保添加的元素至少是T类型或其父类型。

限定通配符的主要作用是增强类型安全并提供更大的灵活性。通过使用限定通配符,你可以在编译时确保泛型类型的正确性,同时避免不必要的类型转换。同时,限定通配符也使得泛型代码更加灵活,能够处理更广泛的类型范围。

6. List< ? extends T > 和 List < ? super T > 之间有什么区别 ?

ListList 在Java的泛型中扮演着不同的角色,它们之间的主要区别在于对泛型类型参数的限制和它们各自的使用场景。

  1. 类型限制

    • List:这个表示法用于设定类型通配符的上限。它表示该列表集合中存放的是类型T或者是T的子类型。由于T可能有多个子类,所以在这种集合中存放的元素只能是T的某个特定子类。这主要用于读取数据的泛型操作,因为它确保读取的元素至少是T类型或其子类型。
    • List:这个表示法用于设定类型通配符的下限。它表示该列表集合中存放的是类型T或者是T的父类型。这意味着向这个列表中添加元素时,只能添加T类型或T的子类型。这在编译期间强制转换成T是类型安全的。
  2. 使用场景

    • List:主要用于读取数据。由于无法确定具体的子类型,因此不能向这样的列表中添加元素。例如,如果你有一个List,你不能向其中添加任何Number的子类实例,因为编译器无法确定列表实际接受的具体子类型。
    • List:主要用于写入数据。虽然可以向这样的列表中添加元素,但由于元素的类型可能是T的任意父类型,因此在读取元素时无法确定其确切类型,因此通常不用于读取操作。例如,你可以向List中添加Integer或Integer的父类实例,但无法从中安全地读取元素。

总的来说,ListList 的主要区别在于它们对泛型类型参数的限制和使用场景。前者主要用于读取操作,确保读取的元素至少是T类型或其子类型;后者主要用于写入操作,允许添加T类型或T的子类型元素,但在读取时存在类型不确定性。

7. Java 中的反射是什么意思?有哪些应用场景?

Java中的反射(Reflection)是一种强大的特性,它允许程序在运行时检查和操作类、对象、方法和属性。反射主要通过java.lang.reflect包中的类来实现。具体来说,反射可以用于获取类的信息,如类名、父类、接口、构造方法、字段和方法,也可以用于动态创建对象,通过调用构造方法来实例化一个类。

反射在Java中有多种应用场景,包括但不限于:

  1. 框架设计:例如,Hibernate等ORM框架在对象持久化时,需要将对象转化为实体类,然后存储到数据库中,这就需要通过反射来动态创建实体类对象、获取类的属性和方法等。
  2. 动态代理:反射机制可以用于实现动态代理,即在运行时动态地生成代理对象,从而实现对目标对象的方法拦截和增强。这在需要对对象的行为进行控制和修改的场景中非常有用,例如实现安全控制、日志记录、性能统计等功能。
  3. 注解处理:注解是Java中非常重要的特性,而注解处理器正是通过反射实现的。通过反射技术可以解析注解信息,并执行相应的操作。
  4. 序列化和反序列化:在Java中,序列化和反序列化都需要使用到反射技术。序列化会将对象转化成字节流,反序列化则将字节流还原为对象。在这个过程中,需要借助反射技术来获取对象的属性信息。
  5. 单元测试:在JUnit等测试框架中,测试类会通过反射获取被测试类的信息并执行测试方法。
  6. 动态加载类:Java中的ClassLoader就是通过反射实现的,可以在运行时动态地加载类,从而实现对类的热加载。

总的来说,反射为Java提供了一种在运行时分析和操作程序的强大手段,使得开发者能够更灵活、更动态地处理类、对象、方法和属性。然而,反射也有其缺点,比如可能会破坏封装性,降低代码的可读性和性能,因此在使用时需要谨慎考虑。

你可能感兴趣的:(Java基础面试题,java,开发语言,面试)