MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java对象的创建与销毁

2023-11-043.7k 阅读

Java对象的创建

在Java中,对象的创建是一个基础且重要的操作,它涉及到内存分配、初始化等多个关键步骤。

1. 使用new关键字创建对象

这是最常见的创建对象的方式。当使用new关键字时,Java虚拟机(JVM)会在堆内存中为对象分配空间,并调用对象的构造函数进行初始化。

下面是一个简单的示例:

class Person {
    private String name;
    private int age;

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

public class ObjectCreationExample {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
    }
}

在上述代码中,new Person("Alice", 30)语句在堆内存中创建了一个Person对象,并调用Person类的构造函数,传入参数"Alice"30来初始化对象的nameage字段。

new关键字的操作过程实际上包含以下几个步骤:

  • 分配内存:JVM在堆内存中为对象分配足够的空间,对象所需的空间大小取决于其成员变量的类型和数量。
  • 默认初始化:在分配内存后,JVM会对对象的成员变量进行默认初始化。例如,数值类型初始化为0,布尔类型初始化为false,引用类型初始化为null
  • 执行构造函数:调用对象的构造函数,构造函数中的代码会对对象进行显式初始化,将成员变量设置为程序员期望的值。

2. 使用Class类的newInstance方法

Class类提供了newInstance方法来创建对象。这种方式通过反射机制来实现对象的创建。

示例代码如下:

class Animal {
    public Animal() {
        System.out.println("Animal object created.");
    }
}

public class ReflectionObjectCreation {
    public static void main(String[] args) {
        try {
            Class<?> animalClass = Class.forName("Animal");
            Animal animal = (Animal) animalClass.newInstance();
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,首先通过Class.forName("Animal")获取Animal类的Class对象,然后调用newInstance方法创建Animal对象。

newInstance方法只能调用类的无参构造函数。如果类没有无参构造函数,会抛出InstantiationException。同时,调用此方法的代码需要处理可能抛出的异常。这种方式通常用于框架开发中,当在运行时才能确定要创建的对象类型时使用。

3. 使用Constructor类的newInstance方法

Constructor类也提供了newInstance方法,与Class类的newInstance方法类似,但它可以调用类的带参数构造函数。

示例如下:

class Fruit {
    private String name;

    public Fruit(String name) {
        this.name = name;
        System.out.println("Fruit " + name + " created.");
    }
}

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class ConstructorObjectCreation {
    public static void main(String[] args) {
        try {
            Constructor<Fruit> constructor = Fruit.class.getConstructor(String.class);
            Fruit fruit = constructor.newInstance("Apple");
        } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

在这段代码中,首先通过Fruit.class.getConstructor(String.class)获取Fruit类的带String参数的构造函数对象,然后调用newInstance("Apple")创建Fruit对象并传入参数"Apple"

这种方式在需要动态调用带参数构造函数时非常有用,同样,使用时需要处理可能抛出的各种异常。

4. 使用clone方法创建对象

如果一个类实现了Cloneable接口,就可以通过clone方法创建对象的副本。clone方法创建的对象与原对象在内存中有独立的副本,但对象内部的引用类型成员变量指向相同的对象(浅拷贝)。

示例代码如下:

class Rectangle implements Cloneable {
    int width;
    int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

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

public class CloneObjectCreation {
    public static void main(String[] args) {
        Rectangle rect1 = new Rectangle(10, 20);
        try {
            Rectangle rect2 = (Rectangle) rect1.clone();
            System.out.println("rect1 width: " + rect1.width + ", rect2 width: " + rect2.width);
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,Rectangle类实现了Cloneable接口并重写了clone方法。rect1.clone()创建了rect1的一个副本rect2。如果要实现深拷贝,即对象内部的引用类型成员变量也有独立的副本,需要手动处理这些引用类型成员变量的复制。

5. 使用反序列化创建对象

当对象被序列化后,可以通过反序列化来创建对象。序列化是将对象转换为字节流的过程,而反序列化则是将字节流重新转换为对象。

示例代码如下:

import java.io.*;

class Book implements Serializable {
    private String title;
    private String author;

    public Book(String title, String author) {
        this.title = title;
        this.author = author;
    }
}

public class DeserializationObjectCreation {
    public static void main(String[] args) {
        Book book1 = new Book("Effective Java", "Joshua Bloch");

        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("book.ser"))) {
            oos.writeObject(book1);
        } catch (IOException e) {
            e.printStackTrace();
        }

        Book book2 = null;
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("book.ser"))) {
            book2 = (Book) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }

        if (book2 != null) {
            System.out.println("Book title: " + book2.title + ", author: " + book2.author);
        }
    }
}

在上述代码中,首先创建了一个Book对象book1并将其序列化到文件book.ser中。然后通过反序列化从文件中读取字节流并创建book2对象。对象的反序列化过程会重新创建对象,并恢复对象的状态,前提是类实现了Serializable接口。

Java对象的销毁

在Java中,对象的销毁不像C++等语言那样需要程序员手动管理。Java有自动垃圾回收(Garbage Collection,GC)机制来处理对象的销毁,回收不再使用的对象所占用的内存。

1. 垃圾回收机制概述

垃圾回收器(GC)负责在JVM运行过程中自动检测并回收不再被使用的对象所占用的内存。垃圾回收器运行的基本原理是通过跟踪对象的引用关系,确定哪些对象不再被任何活动的引用所指向,这些对象就被认为是垃圾,可以被回收。

JVM中有多种垃圾回收算法,常见的有标记 - 清除算法、标记 - 整理算法、复制算法等。

标记 - 清除算法

  • 标记阶段:垃圾回收器从根对象(如栈中的局部变量、静态变量等)开始遍历,标记所有被引用的对象。
  • 清除阶段:遍历堆内存,回收所有未被标记的对象所占用的内存空间。这种算法的缺点是会产生内存碎片,即回收后的内存空间是不连续的,可能导致后续大对象无法分配足够的连续内存。

标记 - 整理算法

  • 标记阶段与标记 - 清除算法相同,从根对象开始标记所有被引用的对象。
  • 整理阶段:将所有存活的对象向内存的一端移动,然后直接清除边界以外的内存空间。这样可以避免内存碎片问题,但整理过程需要移动对象,开销较大。

复制算法

  • 将内存空间划分为两个相等的区域,每次只使用其中一个区域。
  • 当一个区域的内存使用完后,将存活的对象复制到另一个区域,然后清除原区域的所有对象。这种算法不会产生内存碎片,且复制过程相对简单,但会浪费一半的内存空间。

现代JVM通常采用分代垃圾回收机制,结合多种垃圾回收算法。根据对象的存活时间将堆内存分为不同的代,如新生代、老年代等。不同代采用不同的垃圾回收算法,以提高垃圾回收的效率。

2. 对象何时被视为垃圾

一个对象在Java中被视为垃圾,当它不再被任何活动的引用所指向。例如:

public class GarbageExample {
    public static void main(String[] args) {
        String str1 = new String("Hello");
        String str2 = str1;
        str1 = null;
        // 此时"Hello"字符串对象还被str2引用,不会被视为垃圾
        str2 = null;
        // 此时"Hello"字符串对象不再被任何引用指向,可能会被视为垃圾
    }
}

在上述代码中,当str1str2都被赋值为null后,"Hello"字符串对象不再有任何活动的引用,就可能会被垃圾回收器视为垃圾并回收其占用的内存。

然而,对象的引用关系可能会很复杂。例如,对象之间可能存在循环引用:

class A {
    B b;
}

class B {
    A a;
}

public class CircularReferenceExample {
    public static void main(String[] args) {
        A a = new A();
        B b = new B();
        a.b = b;
        b.a = a;
        a = null;
        b = null;
        // 虽然a和b被设为null,但A和B对象之间存在循环引用,
        // 垃圾回收器需要特殊处理才能正确回收它们占用的内存
    }
}

在这种情况下,尽管ab被设为null,但AB对象之间存在相互引用,传统的从根对象开始的标记算法可能会误判它们为存活对象。现代的垃圾回收器通常能够处理这种循环引用的情况,通过更复杂的算法来正确识别并回收这些对象。

3. 垃圾回收的触发时机

垃圾回收器的触发时机是由JVM自动管理的,程序员通常无法精确控制。一般来说,当堆内存中的可用空间不足时,垃圾回收器会被触发。

JVM也提供了一些方法来建议垃圾回收器运行,如System.gc()Runtime.getRuntime().gc()。但需要注意的是,这只是建议垃圾回收器运行,并不保证垃圾回收器一定会立即执行。

public class SuggestGCExample {
    public static void main(String[] args) {
        // 创建大量对象,使堆内存接近满负荷
        for (int i = 0; i < 1000000; i++) {
            new Object();
        }
        // 建议垃圾回收器运行
        System.gc();
    }
}

在上述代码中,通过创建大量对象使堆内存接近满负荷,然后调用System.gc()建议垃圾回收器运行。但垃圾回收器可能会根据自身的策略和当前系统状态决定是否立即执行。

4. 终结方法(Finalize Method)

在Java中,每个对象都有一个finalize方法。当垃圾回收器准备回收对象时,会先调用对象的finalize方法(如果存在)。finalize方法的主要作用是在对象被销毁前执行一些清理操作,如关闭文件、释放资源等。

示例代码如下:

class ResourceHolder {
    private File file;

    public ResourceHolder(String filePath) {
        try {
            file = new File(filePath);
            // 模拟打开文件资源
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void finalize() throws Throwable {
        if (file != null && file.exists()) {
            // 模拟关闭文件资源
            System.out.println("Closing file: " + file.getAbsolutePath());
        }
        super.finalize();
    }
}

public class FinalizeExample {
    public static void main(String[] args) {
        ResourceHolder holder = new ResourceHolder("test.txt");
        holder = null;
        // 建议垃圾回收器运行,可能会调用holder对象的finalize方法
        System.gc();
    }
}

在上述代码中,ResourceHolder类重写了finalize方法,在方法中模拟关闭文件资源的操作。当holder对象不再被引用且垃圾回收器准备回收它时,会调用finalize方法。

然而,finalize方法存在一些问题。首先,它的执行时机不确定,可能会导致资源释放不及时。其次,finalize方法的调用可能会影响垃圾回收的性能,因为垃圾回收器需要等待finalize方法执行完毕才能真正回收对象。因此,在Java 9及以后的版本中,finalize方法已被标记为过时,推荐使用AutoCloseable接口和try - with - resources语句来进行资源管理。

例如:

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

class FileResource implements AutoCloseable {
    private FileInputStream fis;

    public FileResource(String filePath) throws IOException {
        File file = new File(filePath);
        fis = new FileInputStream(file);
    }

    @Override
    public void close() throws IOException {
        if (fis != null) {
            fis.close();
        }
    }
}

public class TryWithResourcesExample {
    public static void main(String[] args) {
        try (FileResource resource = new FileResource("test.txt")) {
            // 使用文件资源
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,FileResource类实现了AutoCloseable接口,在try - with - resources语句块结束时,会自动调用close方法来关闭文件资源,这种方式更加可靠和高效。

对象创建与销毁的性能考虑

在Java编程中,对象的创建和销毁对程序的性能有显著影响。

1. 对象创建的性能影响

  • 内存分配开销:每次使用new关键字创建对象时,JVM需要在堆内存中为对象分配空间。频繁创建大量小对象会导致内存分配的开销增加,因为JVM需要不断地寻找合适的内存块来分配。例如,在一个循环中创建大量的字符串对象:
public class StringCreationPerformance {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            String str = new String("temp");
        }
        long endTime = System.currentTimeMillis();
        System.out.println("Time taken: " + (endTime - startTime) + " ms");
    }
}
  • 构造函数执行开销:对象的构造函数中可能包含复杂的初始化逻辑,如数据库连接、文件读取等操作。这些操作会增加对象创建的时间。例如:
class DatabaseConnection {
    private Connection conn;

    public DatabaseConnection() {
        try {
            Class.forName("com.mysql.jdbc.Driver");
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

public class ConnectionCreationPerformance {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 10; i++) {
            DatabaseConnection conn = new DatabaseConnection();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("Time taken: " + (endTime - startTime) + " ms");
    }
}

为了优化对象创建的性能,可以考虑以下几点:

  • 对象池:对于频繁创建和销毁的对象,可以使用对象池技术。对象池预先创建一定数量的对象,当需要使用时从对象池中获取,使用完毕后再放回对象池,避免了频繁的对象创建和销毁。例如,数据库连接池就是一种对象池的应用。
  • 减少不必要的对象创建:尽量复用已有的对象,避免在不必要的情况下创建新对象。例如,使用StringBuilder来处理字符串拼接,而不是每次都创建新的String对象。

2. 对象销毁的性能影响

  • 垃圾回收开销:垃圾回收器在回收对象时需要消耗一定的系统资源,包括CPU时间和内存。频繁创建和销毁对象会导致垃圾回收器频繁运行,增加系统开销。例如,在一个高并发的Web应用中,如果大量的请求处理过程中创建了大量临时对象,垃圾回收器可能会频繁运行,影响系统的响应时间。
  • 终结方法开销:如前面提到的,finalize方法的执行会影响垃圾回收的性能,因为垃圾回收器需要等待finalize方法执行完毕才能真正回收对象。如果finalize方法中包含复杂的操作,会进一步增加垃圾回收的时间。

为了优化对象销毁的性能,可以:

  • 及时释放引用:尽早将不再使用的对象的引用设为null,使垃圾回收器能够尽快识别并回收这些对象。
  • 避免使用终结方法:尽量使用AutoCloseable接口和try - with - resources语句来进行资源管理,避免依赖finalize方法。

总结对象创建与销毁的要点

在Java中,对象的创建和销毁是编程过程中不可避免的操作。深入理解对象创建的多种方式及其原理,能够帮助我们根据不同的应用场景选择最合适的创建方式,提高代码的灵活性和可维护性。例如,在框架开发中,反射机制创建对象可以提供动态性;而在日常开发中,new关键字是最常用且直接的方式。

对于对象的销毁,虽然Java的垃圾回收机制为我们自动管理内存提供了便利,但了解垃圾回收的原理、对象何时被视为垃圾以及垃圾回收的触发时机等知识,有助于我们编写更高效的代码,避免因对象引用管理不当导致的内存泄漏等问题。同时,注意对象创建和销毁过程中的性能影响,通过优化策略如对象池、及时释放引用等,可以提升程序的整体性能。总之,对Java对象的创建与销毁的深入理解是成为一名优秀Java开发者的重要基础。