Java 基础面试题——面向对象

1.面向对象和面向过程有什么区别?

(1)面向对象 (Object-Oriented Programming,OOP):

  • 把问题拆分成一个个的对象,每个对象都具有特定的属性和方法。对象之间通过消息传递进行协作和交互。
  • 强调封装继承多态等概念,通过类和对象来组织和抽象问题。
  • 将问题领域中的实体抽象为类,并通过类之间的关系来模拟现实世界的系统。
  • 代码重用性高,易于扩展和维护,能提供更好的可靠性可复用性

(2)面向过程 (Procedural Programming):

  • 程序被视为一系列的步骤或函数,按顺序执行从而解决问题。
  • 强调顺序条件循环等基本控制结构。
  • 通过函数调用来组织代码,数据和操作是分离的。
  • 着重于解决问题的算法和过程,对于较小规模、简单问题较为合适。
  • 代码的可读性较强,适合提供直接而快速的解决方案。

(3)总体来说,面向对象编程更加注重问题的抽象和模型化,通过封装、继承和多态等机制提供了更高的灵活性和可扩展性,适用于大型、复杂的系统开发。而面向过程编程更加注重解决问题的具体步骤和流程,适用于较小规模、直接的问题求解。实际上,面向对象编程可以看作是对面向过程编程的一个扩展和演进。在实际开发中,可以根据问题的性质和需求选择合适的编程范式。

2.面向对象有哪些特征?

(1)抽象
抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。

  • 数据抽象:数据抽象是指隐藏对象的数据细节,只向用户暴露必要的信息。也就是说,用户不能直接访问对象的内部属性,而必须通过定义在类中的方法来访问和操作数据。数据抽象通过封装数据来保护数据的完整性和安全性。在 Java 中,数据抽象可以通过访问修饰符来实现,例如 private、public、protected 等。
  • 行为抽象:行为抽象是指抽象出对象的行为方式,实现了把重点放在具体行为上而非具体实现上。也就是说,对象的行为和操作被封装在方法中,用户只需要知道如何调用方法,无需关心具体实现细节。行为抽象通过定义接口或者抽象类来实现,Java 中的接口 (interface) 和抽象类 (abstract class) 都是行为抽象的实现方式。

(2)继承
继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类或基类);得到继承信息的类被称为子类(或派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段。

(3)封装
封装是指将数据和操作封装在一个对象中,通过定义公共接口来访问对象的属性和方法,隐藏内部实现细节,提高代码的安全性和可维护性。。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口。

(4)多态
多态是指同一个方法在不同对象上执行时可能呈现不同的行为。具体而言,多态允许我们使用父类类型的引用来引用子类类型的对象,以及在运行时确定调用哪个方法。Java 中的多态有两种形式:

  • 编译时多态(静态多态,也称为重载):在编译时期,根据方法的参数类型、个数或顺序来决定调用哪个重载的方法。这种多态是通过方法重载实现的。例如:
public class Animal {
    public void makeSound() {
        System.out.println("Animal is making a sound");
    }
    
    public void makeSound(String sound) {
        System.out.println("Animal is making " + sound);
    }
}

public class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog is barking");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Animal();
        animal.makeSound(); // Output: Animal is making a sound
        
        Dog dog = new Dog();
        dog.makeSound(); // Output: Dog is barking
        
        Animal animal2 = new Dog(); // 编译时类型是 Animal,运行时类型是 Dog
        animal2.makeSound(); // Output: Dog is barking
        animal2.makeSound("Woof"); // Output: Animal is making Woof
    }
}
  • 运行时多态(动态多态,也称为重写):在运行时期,根据对象的实际类型来确定调用哪个重写的方法。这种多态是通过方法重写和父类引用指向子类对象实现的。例如:
public class Animal {
    public void makeSound() {
        System.out.println("Animal is making a sound");
    }
}

public class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog is barking");
    }
}

public class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Cat is meowing");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.makeSound(); // Output: Dog is barking
        
        animal = new Cat();
        animal.makeSound(); // Output: Cat is meowing
    }
}

在运行时多态中,使用父类类型的引用指向子类对象时,实际调用的是子类重写的方法。这种多态性允许我们根据实际需要以不同的方式处理对象,提高了代码的灵活性和可维护性。

3.静态变量和实例变量有什么区别?

(1)在 Java 中,静态变量(类变量)和实例变量(成员变量)有以下区别:

  • 定义位置:静态变量定义在类级别,使用 static 关键字修饰,属于类本身;实例变量定义在类中,属于类的每个实例
  • 内存分配:静态变量在类加载时就会被分配内存,只有一份拷贝,被所有的类实例所共享;实例变量在每个类的实例化过程中分配内存,每个实例都有自己的变量拷贝。
  • 访问方式:静态变量可以通过类名直接访问,也可以通过对象引用来访问;实例变量只能通过对象引用来访问。
  • 生命周期:静态变量的生命周期与类的生命周期相同,当类被加载时初始化,直到程序结束或类被卸载;实例变量的生命周期与类的实例相同,当实例被创建时初始化,直到垃圾回收时被销毁。
  • 访问范围:静态变量可以有 public、protected、private 和默认 (package-private) 访问级别;实例变量可以有 public、protected、private 和默认 (package-private) 访问级别,也可以使用final关键字使其成为常量。
  • 用途:静态变量通常用于表示与类相关的特性,如常量、全局变量等;实例变量通常用于表示每个对象实例的状态和属性。

(2)需要注意的是,静态变量属于类而不是实例,因此它们可以在没有任何类实例存在的情况下被访问。但使用过多的静态变量可能导致全局状态的复杂性和不确定性增加,不易于维护和测试。因此,在使用静态变量时,应该确保它们的使用是合理的,并遵循设计原则和最佳实践。

4.✨Java 对象实例化顺序是怎样的?

(1)类加载器实例化时进行的操作步骤:加载 -> 连接 -> 初始化

(2)实例化优先级顺序:父类 > 子类 , 静态代码块 > 非静态代码块 > 构造函数,具体按照如下顺序:

  • 按照代码书写顺序加载父类静态变量父类静态代码块
  • 按照代码书写顺序加载子类静态变量子类静态代码块
  • 父类非静态变量(父类实例成员变量);
  • 父类非静态代码块;
  • 父类构造函数;
  • 子类非静态变量(子类实例成员变量);
  • 子类非静态代码块;
  • 子类构造函数;

(3)举例说明如下:

class A {
    //静态代码块(只初始化一次)
    static {
        System.out.println("父类的静态代码块...");
    }
    
    //非静态代码块
    {
        System.out.println("父类的非静态代码块...");
    }
    
    //构造函数
    public A() {
        System.out.println("父类的构造函数...");
    }
}

class B extends A {
    //静态代码块(只初始化一次)
    static {
        System.out.println("子类的静态代码块...");
    }
    
    //非静态代码块
    {
        System.out.println("子类的非静态代码块...");
    }
    
    //构造函数
    public B() {
        System.out.println("子类的构造函数...");
    }
}

public class Solution {
    public static void main(String[] args) {
        A ab = new B();
        System.out.println("---------");
        ab = new B();
    }
}

上述代码的输出结果如下:

父类的静态代码块...
子类的静态代码块...
父类的非静态代码块...
父类的构造函数...
子类的非静态代码块...
子类的构造函数...
------------
父类的非静态代码块...
父类的构造函数...
子类的非静态代码块...
子类的构造函数...

5.浅拷贝和深拷贝的区别是什么?什么是引用拷贝?

5.1.浅拷贝

  • 对象的成员变量进行逐个复制:
    • 如果成员变量是基本类型,则复制其值;
    • 如果成员变量是引用类型,则复制引用地址(指向同一内存地址)。
  • 浅拷贝只复制对象本身以及对象内部的数据(无论是基本类型还是引用类型的地址),不会复制引用对象本身。
  • 修改浅拷贝对象内部的引用类型成员变量时,会影响到原始对象和浅拷贝对象,因为它们共享同一个引用对象
public class Address {
    int id;
    String addressName;
    
    public int getId() {
        return id;
    }
    
    public String getAddressName() {
        return addressName;
    }
    
    public Address(int id, String addressName) {
        this.id = id;
        this.addressName = addressName;
    }
    
    public void setId(int id) {
        this.id = id;
    }
    
    public void setAddressName(String addressName) {
        this.addressName = addressName;
    }
    
    @Override
    public String toString() {
        return "Address{" +
                "id=" + id +
                ", addressName='" + addressName + '\'' +
                '}';
    }
}
public class Person implements Cloneable {
    private int id;
    private String name;
    private Address address;
    
    public String getName() {
        return name;
    }
    
    public void setId(int id) {
        this.id = id;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public int getId() {
        return id;
    }
    
    public Address getAddress() {
        return address;
    }
    
    public void setAddress(Address address) {
        this.address = address;
    }
    
    public Person(int id, String name, Address address) {
        this.id = id;
        this.name = name;
        this.address = address;
    }
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }
    
    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", address=" + address +
                '}';
    }
}
public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person(1, "小华", new Address(1, "北京"));
        //浅拷贝
        Person person2 = (Person) person1.clone();
        System.out.println("person1 == person2 的结果为:" + (person1 == person2));
    
        System.out.println("\n改变person1的属性值之前:");
        System.out.println("person1:" + person1);
        System.out.println("person2:" + person2);
        System.out.println("person1.getName() == person2.getName()的结果为:" + (person1.getName() == person2.getName()));
        System.out.println("person1.getAddress() == person2.getAddress()的结果为:" + (person1.getAddress() == person2.getAddress()));
        
        person1.setId(2);
        person1.setName("小明");
        person1.getAddress().setId(2);
        person1.getAddress().setAddressName("武汉");
    
        System.out.println("\n改变person1的属性值之后:");
        System.out.println("person1:" + person1);
        System.out.println("person2:" + person2);
        
        System.out.println("person1.getName() == person2.getName()的结果为:" + (person1.getName() == person2.getName()));
        System.out.println("person1.getAddress() == person2.getAddress()的结果为:" + (person1.getAddress() == person2.getAddress()));
    }
}

在这里插入图片描述

5.2.深拷贝

(1)深拷贝

  • 对象的成员变量进行递归复制,不仅复制对象本身,还复制引用类型的对象本身。
  • 深拷贝复制对象及其所有引用对象,每个对象都有自己的一份独立内存
  • 修改深拷贝对象内部的引用类型成员变量时,不会影响原始对象和其他深拷贝对象,因为它们拥有各自独立的引用对象。

(2)实现方式如下:
① 通过对象序列化实现深拷贝(推荐)
先将 Address 类和 Person 类实现 Serializable 接口,然后再修改Test.java中的代码即可。

public class Test {
    public static void main(String[] args) throws Exception {
        Person person1 = new Person(1,"小华",new Address(1,"北京"));
        
        //创建对象输出流对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\testData\\a.txt"));
        //写对象
        oos.writeObject(person1);
        //释放资源
        oos.close();
        
        //创建对象输入流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\testData\\a.txt"));
        //读取对象(深拷贝)
        Person person2 = (Person) ois.readObject();
        //释放资源
        ois.close();
    
        System.out.println("person1 == person2 的结果为:" + (person1 == person2));
    
        System.out.println("\n改变person1的属性值之前:");
        System.out.println("person1:" + person1);
        System.out.println("person2:" + person2);
        System.out.println("person1.getName() == person2.getName()的结果为:" + (person1.getName() == person2.getName()));
        System.out.println("person1.getAddress() == person2.getAddress()的结果为:" + (person1.getAddress() == person2.getAddress()));
    
        person1.setId(2);
        person1.setName("小明");
        person1.getAddress().setId(2);
        person1.getAddress().setAddressName("武汉");
    
        System.out.println("\n改变person1的属性值之后:");
        System.out.println("person1:" + person1);
        System.out.println("person2:" + person2);
    
        System.out.println("person1.getName() == person2.getName()的结果为:" + (person1.getName() == person2.getName()));
        System.out.println("person1.getAddress() == person2.getAddress()的结果为:" + (person1.getAddress() == person2.getAddress()));
    }
}

在这里插入图片描述

② 重写Person 类中的 clone() 方法来实现深拷贝

public class Address implements Cloneable {

   	//...
    
    @Override
    public Object clone() throws CloneNotSupportedException {
        return (Address) super.clone();
    }
    
    //...
}
public class Person implements Cloneable {
    
    //...
    
    @Override
    public Object clone() throws CloneNotSupportedException {
        Person person = null;
        person = (Person)super.clone();
        //对引用数据类型单独处理
        person.name = new String(name);
        person.address = (Address)address.clone();
        return person;
    }
    
    //...
}

5.3.引用拷贝

(1)引用拷贝 (Reference Copy) 是一种浅拷贝的方式,即复制变量指向的地址和值,使得变量和拷贝后的变量指向同一个对象。举个例子,假设我们有以下代码:

class Person {
  public int age;
  public String name;
}
 
public class Main {
  public static void main(String[] args) {
    Person p1 = new Person();
    p1.age = 18;
    p1.name = "Alice";
    Person p2 = p1; // 引用拷贝
    p2.age = 20;
    p2.name = "Bob";
    System.out.println(p1.age); // 20
    System.out.println(p1.name); // Bob
  }
}

在上面的代码中,我们创建了一个名为 Person 的类,其中有两个成员变量 agename。我们在 main 方法中创建一个名为 p1 的 Person 对象,设置 age 和 name 属性分别为 18 和 “Alice”。 然后又创建一个名为 p2 的 Person 对象,并将 p2 指向 p1,即 p2 = p1,这是引用拷贝。之后,我们修改了 p2 的属性值,将 age 和 name 属性分别改为 20 和 “Bob”。由于 p2 是对 p1 的引用拷贝,因此对 p2 的修改会影响到 p1。最后,我们尝试打印 p1 的属性值,发现 age 和 name 属性的值也发生了改变,和 p2 的属性值相同。

(2)上述例子中的引用拷贝并没有复制对象,而是复制了对象的引用地址,因此只有一个 Person 对象,只是这个对象被多个引用指向了而已。所以当我们修改 p2 指向的对象时,也相当于修改 p1 指向的对象。

5.4.总结

操作是否指向同一个堆内存地址基本数据类型引用数据类型
赋值会改变会改变
浅拷贝不会改变会改变
深拷贝不会改变不会改变

6.✨Java 中创建对象的方式有哪几种?

6.1.使用 new 关键字

这是最常见也是最简单的创建对象的方式,通过这种方式,我们可以调用任意的构造函数(无参的和有参的)。

public class Student {
    public static void main(String[] args) {
        Student student = new Student();
    }
}

有关使用 new 关键字创建对象的具体过程可以参考 JVM 面试题——Java 内存区域与内存溢出这篇文章。

6.2.通过反射机制

  • 使用 Class 类中的 newInstance 方法:使用 Class 类中的 newInstance 方法创建对象。这个 newInstance 方法调用无参的构造函数创建对象。
public class Student {
    public static void main(String[] args) throws Exception {
        //使用 Class 类中的 newInstance 方法创建对象,有以下两种方式
        Student student1 = (Student) Class.forName("test.Student").newInstance();
        Student student2 = Student.class.newInstance();
        System.out.println(student1 == student2);           //false
    }
}
  • Constructor 类中的 newInstance 方法:和 Class 类中的 newInstance 方法很像, java.lang.reflect.Constructor 类里也有一个 newInstance 方法可以创建对象。我们可以通过这个newInstance 方法调用有参数的和私有的构造函数。
public class Student {
    public static void main(String[] args) throws Exception {
        Constructor<Student> constructor = Student.class.getConstructor();
        Student student = constructor.newInstance();
    }
}

有关反射的相关知识可以参考 Java 基础——反射这篇文章。

6.3.使用 clone 方法

无论何时我们调用一个对象的 clone(),JVM 就会创建一个新的对象,将前面对象的内容全部拷贝进去。用 clone() 创建对象并不会调用任何构造函数。要使用 clone(),我们需要先实现 Cloneable 接口并实现其定义的 clone()

public class Student implements Cloneable {
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        System.out.println("具体原型复制成功!");
        return (Student)super.clone();
    }
    
    public static void main(String[] args) throws CloneNotSupportedException {
        Student student1 = new Student();
        Student student2 = (Student)student1.clone();
        System.out.println(student1 == student2);       //false
    }
}

相关知识点:
Java 设计模式——原型模式

6.4.通过序列化机制

  • 序列化就是把对象通过流的方式存储到文件中,此对象要实现 Serializable 接口才能被序列化。
  • 反序列化就是把文件中的内容读取出来,还原为 Java 对象,该过程也需要实现 Serializable 接口。
  • 当我们序列化和反序列化一个对象,JVM 会给我们创建一个单独的对象。在反序列化时,JVM 创建对象并不会调用任何构造函数。
public class Student implements Serializable {
    
    private int age;
    
    public int getAge() {
        return age;
    }
    
    public void setAge(int age) {
        this.age = age;
    }
    
    public static void main(String[] args) throws Exception {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("stu.txt"));
        Student student1 = new Student();
        student1.setAge(18);
        out.writeObject(student1);
        
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("stu.txt"));
        Student student2 = (Student) in.readObject();
        System.out.println(student2.getAge());              //18
	
		System.out.println(student1 == student2);           //false
    }
}

相关知识点:
Java 基础——File 类与 I/O 流

7.✨重写和重载的区别是什么?

7.1.重写 (Override)

从字面上看,重写就是重新写一遍的意思。其实就是在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法, 此时可以对方法体进行修改或重写。但需要注意以下几点,简称两同两小加一大原则:

  • 方法名、参数列表相同;
  • 子类返回类型小于等于父类方法返回类型、重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常
  • 子类函数的访问修饰权限要大于等于父类的 (public > protected > default > private)
public class Father {
    public void sayHello() {
        System.out.println("Hello, Father");	//Hello, Son
    }
    
    public static void main(String[] args) {
        Son s = new Son();
        s.sayHello();
    }
}

class Son extends Father{
    @Override
    public void sayHello() {
        // TODO Auto-generated method stub
        System.out.println("Hello, Son");
    }
}

7.2.重载 (Overload)

在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)则视为重载,它是一个类中多态性的一种表现。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载。

public class Father {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Father s = new Father();
        s.sayHello();
        s.sayHello("wintershii");
    }
    public void sayHello() {
        System.out.println("Hello");
    }
    public void sayHello(String name) {
        System.out.println("Hello" + " " + name);
    }
}

注意:重载是编译时多态,而重写是运行时多态。

8.✨Java 的四种引用方式分别是什么?

(1)强引用 (Strong Reference)
强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似下面代码中的这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

//使用方式
Object obj = new Object()

(2)软引用 (Soft Reference)

  • 软引用在程序内存不足时会被回收。
  • 可用场景: 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM 就会回收早先创建的对象。
/*
	注意:wrf 这个引用也是强引用,它是指向 SoftReference 这个对象的,
	这里的软引用指的是指向new String("str")的引用,也就是 SoftReference 类中T
*/
SoftReference<String> wrf = new SoftReference<String>(new String("str"));

(3)弱引用 (Weak Reference)

  • 弱引用通过 WeakReference 类实现,被弱引用关联的对象只能生存到下一次垃圾回收之前。当垃圾回收器进行垃圾回收时,无论内存是否充足,只要对象只被弱引用引用,该对象就会被回收。
  • 使用场景:弱引用通常用于实现内存敏感的缓存、观察者模式等场景。Java 源码中 java.util.WeakHashMap 中的 key 就是使用弱引用,一旦我不需要某个引用,JVM 会自动帮忙处理它,这样我们就不需要做其它操作。
//使用方式
WeakReference<String> wrf = new WeakReference<String>(str);

(4)虚引用 (Phantom Reference)

  • 虚引用通过 PhantomReference 类实现,虚引用是最弱的一种引用关系。虚引用在任何时候都可能被垃圾回收器回收,但是它被回收之前,会被放入 ReferenceQueue 中。需要注意的是,其它引用是被 JVM 回收后才被传入 ReferenceQueue 中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。此外,虚引用创建的时候,必须带有 ReferenceQueue。
  • 可用场景: 对象销毁前的一些操作,比如说资源释放等。 Object.finalize() 虽然也可以做这类动作,但是这个方式即不安全又低效。
//使用方式
PhantomReference<String> prf = new PhantomReference<String>(new String("str"), new ReferenceQueue<>());

注意:上面所说的几类引用,都是指对象本身的引用,而不是指 Reference 的四个子类的引用。

9.构造方法有哪些特点?

(1)名字必须与类名相同。
(2)没有返回值,也不能用 void 声明构造函数。
(3)生成类的对象时自动调用,且在整个对象的生命周期内只调用一次
(4)构造方法不能被 override(重写),但是可以 overload(重载),所以我们经常可以看到一个类中有多个构造方法的情况。
(5)如果用户没有显式定义构造方法,编译器就会默认生成一份构造方法,而且默认生成的构造方法一定是无参的。不过一旦用户定义了一个构造方法,编译器咋不会自动生成的构造方法。

10.✨抽象类 (abstract class) 和接口 (interface) 有什么共同点和区别?

(1)共同点

  • 都不能被实例化;
  • 都可以包含抽象方法;
  • 都可以有默认实现的方法(JDK 1.8 可以用 default 关键字在接口中定义默认方法);

(2)区别

  • 基本使用
    • 抽象类只能继承一个,接口可以实现多个,即单继承,多实现
    • 接口比抽象类更加抽象,因为抽象类中可以定义构造方法,可以有抽象方法和具体方法。而接口中不能定义构造方法,而且其中的方法全部都是抽象方法,默认且只能是 public abstract 的;
      在这里插入图片描述
    • 抽象类中可以定义静态块,而接口中则不能;
    • 抽象类中可以定义成员变量,并且接口中定义的成员变量实际上都是常量,默认且只能是 public static final 的;
      在这里插入图片描述
    • 有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法;
  • 设计目的
    • 接口的设计目的是对类的行为进行约束(更准确的说是一种"有"约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现行为进行限制。也可以将接口理解为对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。

    • 抽象类的设计目的是代码复用。当不同的类具有某些相同的行为(记为行为集合 A),且其中一部分行为的实现方式一致时(A 的非真子集,记为 B),可以让这些类都派生于一个抽象类。在这个抽象类中实现了 B,避免让所有的子类来实现 B,这就达到了代码复用的目的。而 A - B 的部分,留给各个子类自己实现。正是因为 A - B 在这里没有实现,所以抽象类不允许实例化出来(否则当调用到 A - B时,无法执行)。

  • 设计思想
    • 抽象类是对类本质的抽象(自下而上),表达的是 is xxx 的关系,比如: BMW is a car。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。
    • 而接口是对行为的抽象(自上而下),表达的是 like xxx 的关系。比如:Bird like a Aircraft(像飞行器一样可以飞),但其本质上 is a Bird。接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现的,接口并不关心。
  • 使用场景:当关注事物的本质时,使用抽象类;当关注事物的操作时,使用接口。
  • 复杂度:抽象类的功能要远超过接口,但是定义抽象类的代价比较高。因为对于Java来说,每个类只能继承一个类,在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口,在设计阶段会降低难度。

11.什么是内部类?为什么需要使用内部类?

(1)内部类是 Java 中的一种语法特性,它可以在一个类的内部定义另一个类,并与外部类存在一定的关系。在 Java 中,内部类分为四种:成员内部类、静态内部类、局部内部类和匿名内部类。

  • 成员内部类:成员内部类是在一个类的内部定义的类,它可以访问外部类的成员和方法。成员内部类可以使用 public、protected、private 等访问修饰符。示例代码如下:
public class OuterClass {
    private String name;
    
    public class InnerClass {
        public void printName() {
            System.out.println(name);
        }
    }
}
  • 静态内部类:静态内部类是在一个类的内部定义的静态类,它可以没有外部类的实例而存在。静态内部类不可以访问外部类的非静态成员。示例代码如下:
public class OuterClass {
    public static class InnerClass {
        // 静态内部类
    }
}
  • 局部内部类:局部内部类是在方法或代码块内部定义的类,局部内部类只在该方法或代码块内有效。示例代码如下:
public class OuterClass {
    public void printMyName(String name) {
        class MyName {
            public void print() {
                System.out.println(name);
            }
        }
        new MyName().print();
    }
}
  • 匿名内部类:匿名内部类是一种没有类名的内部类,直接使用 new 关键字定义,通常用于实现某个接口或继承某个类,可以在定义的同时直接实例化对象。示例代码如下:
public class OuterClass {
    public void printMyName(String name) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(name);
            }
        }).start();
    }
}

在上述代码中,使用匿名内部类实现了 Runnable 接口,并通过它启动了一个新的线程。

总之,内部类是 Java 中十分有用的语法特性,它可以让我们轻松地实现封装和代码重用,特别是匿名内部类,在实现一些简单接口或者回调函数时非常有用。

(2)使用内部类的主要原因有以下三点:

  • 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
  • 内部类可以对同一个包中的其他类隐藏起来。
  • 当想要定义一个回调函数且不想编写大量代码时,使用匿名 (anonymous) 内部类比较便捷。

12.匿名内部类可以继承类或实现接口吗?为什么?

(1)匿名内部类本质上是对父类方法的重写或对接口方法的实现。从语法角度看,匿名内部类创建处无法使用关键字继承类或实现接口

(2)原因如下:

  • 匿名内部类没有名字,所以它没有构造函数。因为没有构造函数,所以它必须通过父类的构造函数来实例化。即匿名内部类完全把创建对象的任务交给了父类去完成。
  • 匿名内部类里创建新的方法没有太大意义,新方法无法被调用。
  • 匿名内部类一般是用来覆盖父类的方法。
  • 匿名内部类没有名字,所以无法进行向下的强制类型转换,只能持有匿名内部类对象引用的变量类型的直接或间接父类。

13.局部内部类和匿名内部类访问局部变量的时候,为什么局部变量必须要用 final 修饰?

(1)局部内部类和匿名内部类在访问局部变量时,要求被访问的局部变量必须使用 final 修饰是因为在内部类中访问局部变量时,实际上是创建了一个拷贝副本。如果不使用 final 修饰,就存在以下问题:

  • 生命周期问题:如果局部变量不是 final 的,那么当外部方法执行完毕后,局部变量可能已经不存在了,而内部类对象可能仍然存在引用该变量的情况下,这将导致访问的变量无效。
  • 内部类与局部变量的作用域问题:内部类对象可以在局部变量的作用域之外被使用,这意味着内部类持有对局部变量的引用,而局部变量在方法结束后会被销毁。如果局部变量不是 final 的,那么在内部类中使用局部变量时,可能出现局部变量值改变而内部类中并不知道的情况。

(2)使用 final 修饰局部变量后,编译器会确保在访问该变量时,其数值不能发生改变,从而避免了上述问题。需要注意的是,如果使用的是Java 8及以上版本,由于编译器的优化,对于不被内部类修改的局部变量,不一定需要显式添加 final 关键字。

class Main {
    public void fun(final int b) {
        final int a = 10;
        new Thread(){
            public void run() {
                System.out.println(a);
                System.out.println(b);
            };
        }.start();
    }
}

14.什么是序列化、反序列化?

(1)序列化 (Serialization) 是将对象转化为字节流的过程,以便将其存储到文件或在网络中传输。反序列化 (Deserialization) 则是将字节流恢复为对象的过程。在 Java 中,可以通过实现 Serializable 接口来实现序列化和反序列化。Serializable 接口是一个标记接口,没有定义任何方法,只是起到一个标记作用,表明类可以被序列化

(2)要实现序列化和反序列化,可以遵循以下步骤:

  • 实现 Serializable 接口:在需要序列化的类上实现 Serializable 接口。
import java.io.Serializable;

public class MyClass implements Serializable {
    // 类的成员变量和方法
}
  • 序列化对象:使用 ObjectOutputStream 将对象序列化为字节流,并将字节流写入文件或输出流中。
MyClass obj = new MyClass();
OutputStream os = new FileOutputStream("object.ser");
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(obj);
oos.close();
os.close();
  • 反序列化对象:使用 ObjectInputStream 从字节流中读取数据,并将其转换为对象。
InputStream is = new FileInputStream("object.ser");
ObjectInputStream ois = new ObjectInputStream(is);
MyClass newObj = (MyClass) ois.readObject();
ois.close();
is.close();

需要注意的是,被序列化的类必须实现 Serializable 接口,且类的所有成员变量也必须是可序列化的。如果某个成员变量不应该被序列化,可以使用 transient 关键字进行标记,该变量在序列化过程中会被忽略。

(3)另外,还可以自定义序列化过程,通过实现 writeObject()readObject() 方法来控制对象的序列化和反序列化行为。这在需要处理特定逻辑或加密操作时非常有用。最后,需要注意不同版本的 Java 可能存在兼容性问题,特别是对于序列化和反序列化的类结构进行了修改的情况。为了确保兼容性,可以使用 serialVersionUID 字段来显式指定序列化版本号,并且在进行修改时进行适当管理。

15.Java 中参数传递是值传递还是引用传递,还是两者共存?

Java 中参数传递只有值传递,没有引用传递。具体分析见 Java 中只存在值传递这篇文章。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码星辰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值