目录
1. new 关键字
1.1无参构造函数调用
1.2带参构造函数调用
2. 克隆技术
2.1先了解区别
2.2浅拷贝
2.3深拷贝
3. 反射
1.通过类名.class
2.通过类的加载器ClassLoader.loadClass() 加载
3.Class.forName()会执行类中的静态块
4.通过new 对象().getClass()
4.反序列化
5. 工厂模式: 建议可以合并到new 关键字
5.1工厂模式和直接new的区别
5.2用代码来区分他们
1. new 关键字
定义:在 Java 中,new 关键字用于实例化一个对象,并调用其构造函数进行初始化。
基本语法:
ClassName objectName = new ClassName(arguments);
ClassName:要实例化的类名。objectName:对象的引用变量名。arguments:传递给构造函数的参数列表(若无参数可省略)。
工作原理:
在 堆内存 中为新对象分配内存空间。调用对象的构造函数进行初始化。返回对象的引用(内存地址)。根据参数匹配对应的构造函数。
1.1无参构造函数调用
如果类定义了无参构造函数(或未显式定义构造函数,使用默认构造),直接使用 new 即可。
public class Person {
public Person() {
System.out.println("无参构造函数调用");
}
}
// 创建对象
Person p = new Person();
1.2带参构造函数调用
若类定义了带参构造函数,需传入对应参数:
public class Student {
private String name;
public Student(String name) {
this.name = name;
System.out.println("带参构造函数调用,name: " + name);
}
}
// 创建对象并传递参数
Student s = new Student("Alice");
2. 克隆技术
克隆是指创建一个对象的精确副本。这个副本应该是一个全新的对象,但它的初始状态与原始对象完全相同。
2.1先了解区别
在 Java 中,直接使用赋值操作符 = 并不能实现克隆,它只会复制对象的引用,而不是对象本身。
Dog dog1 = new Dog("Tom");
Dog dog2 = dog1; // 这不是克隆!
// dog1 和 dog2 指向同一个对象
dog1.setName("Max");//改变dog1的值
System.out.println(dog2.getName()); // dog2 输出 "Max"
用一个生活例子更方便大家对,=号直接赋值,浅拷贝和深拷贝的区别:
拿书来说。等号赋值就像用复印机复印一本书,复印出来的书和原书是完全一样的,它们本质上是同一份内容,你在复印本上做修改,原书也会相应改变,就像等号赋值后,两个变量指向同一个对象,修改一个,另一个也会变。
浅克隆呢,好比你照着一本书重新抄写一遍,外表看起来和原书很像,内容也一样,但是这是两本独立的书了,你修改抄的这本书,原书不会变。不过,如果原书里夹了一张珍贵的照片,你抄书的时候直接把照片从原书拿过来放到新书里了,这张照片就是共享的,不管你在哪个书里动这张照片,另一个书里的照片也会变,这就类似于浅克隆中引用类型的数据共享。
深克隆呢,就更彻底啦。你不仅照着原书抄写内容,书里夹的照片也重新拍了一张放进去,所有东西都是全新的独立的。你对这本新抄的书做任何改动,都完全不会影响到原书,这就是深克隆能做到的,完全独立的复制。
那么又有人问了,这仨有何区别呢,啥时候用呢??
想象一下,你是一个设计师,正在做一个项目。这个项目有一个主设计图,里面包含了:
基本信息:项目名称、客户、日期等。设计元素:一些标准的图形(比如一个圆形、一个方形),这些图形是你从公司的 “素材库” 里直接拖过来的。
现在,客户让你做一个相似的项目 B,它的基本信息需要改,但大部分设计元素(那个圆形和方形)可以直接用。
场景一: “等于号” 赋值
这就像你直接在主设计图上改客户信息。
操作:把项目 A 改名为项目 B,客户信息改成新客户。结果:你原来的项目 A 的文件也变成了项目 B!因为它们是同一个文件。这显然是灾难。
这对应了 Java 中 b = a; 的情况,两个变量指向同一个对象,任何修改都共享。
场景二:使用 “深拷贝”
这就像你从头到尾重新画一份完整的设计图。
操作:新建一个文件,重新输入项目 B 的基本信息,然后从素材库里重新拖一个圆形、一个方形过来,并且重新画一遍。结果:项目 A 和项目 B 是完全独立的。你可以随意修改项目 B 的任何东西,包括把圆形改成红色,都不会影响项目 A。问题:效率太低了!如果设计图非常复杂,重新画一遍会浪费大量时间。
这对应了 Java 中的深拷贝,所有对象,包括嵌套的对象,都会被复制一份新的。
场景三:使用 “浅拷贝”
这就像你复制一份主设计图文件。
操作:右键点击 “主设计图” 文件,选择 “复制”,然后 “粘贴”,得到一个 “主设计图 - 副本”。你打开这个副本,把客户信息改成项目 B 的。结果:
你得到了一个全新的文件(新的内存对象)。修改副本的文件名、客户信息,不会影响原来的 “主设计图” 文件。但是,当你打开副本时,里面的圆形和方形,和原文件里的是同一个东西(指向同一个内存对象)。它们是你从素材库里引用过来的,并没有被重新画一遍。 优点:
高效:复制文件的速度非常快,你不需要重新画那些复杂的图形。满足需求:只要你不打算修改那些共享的设计元素(圆形和方形),浅拷贝就是完美的。你只关心修改项目的 “外壳”(基本信息)。
2.2浅拷贝
浅拷贝是最基础的克隆方式。它创建一个新对象,并将原始对象的所有字段值直接复制到新对象中。
工作原理:
如果字段是基本数据类型(如 int, double, boolean),则复制其值。如果字段是引用数据类型(如对象、数组),则复制其引用地址,而不是引用所指向的对象本身。
这意味着,原始对象和克隆对象会共享这些引用类型的字段。修改其中一个对象的引用字段,会影响到另一个对象。
如何实现:
让类实现 java.lang.Cloneable 接口。这是一个标记接口(Marker Interface),本身没有任何方法,它只是告诉 JVM 这个类的对象是可以被克隆的。重写 java.lang.Object 类中的 clone() 方法,并将其访问修饰符改为 public。
代码示例:
public class Teacher {
String name;
int age;
public Teacher(String name,int age)
{
this.name = name;
this.age =age;
}
}
public class Stu implements Cloneable{
String name;
int age;
Teacher teacher;
public Stu(){
}
public Stu(String name,int age,Teacher teacher){
this.name=name;
this.age=age;
this.teacher=teacher;
}
@Override
public Stu clone() throws CloneNotSupportedException {
Stu stu = (Stu) super.clone();// 浅拷贝
return stu;
}
}
public class Test {
public static void main(String[] args) {
Teacher t = new Teacher("王老师", 38);
Stu stu = new Stu("张三",19, t);
//克隆对象,创建对象,但是不调用构造函数
try {
Stu s1 = stu.clone() ;
//由于这里克隆开辟了新的空间,所以两个对象不相等
System.out.println(stu == s1);// false
System.out.println(s1.name + " " + s1.age + " " + s1.teacher.name + " " + s1.teacher.age);// 张三 19 王老师 38
//浅度克隆,克隆基本数据类型变量,不克隆引用类型变量
s1.teacher.name = "陈老师";
s1.teacher.age = 28;
System.out.println(stu.name + " " + stu.age + " " + stu.teacher.name + " " + stu.teacher.age);// 张三 19 陈老师 28
System.out.println(s1.name + " " + s1.age + " " + s1.teacher.name + " " + s1.teacher.age);// 张三 19 陈老师 28
//代码在这里很多人就会疑问,为啥String明明是引用类型,为啥效果却像深度克隆呢。
stu.name = "王五";
System.out.println(stu.name + " " + stu.age + " " + stu.teacher.name + " " + stu.teacher.age);
System.out.println(s1.name + " " + s1.age + " " + s1.teacher.name + " " + s1.teacher.age);
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
我来对该代码中String进行一个解释:
String 的特殊性: String 是引用类型:String 在 Java 中确实是引用类型,但它具有不可变性的特殊特性。 克隆中的 String 处理: 浅拷贝行为:在 clone() 过程中,String 类型的字段(如 name)确实只是复制了引用 安全性保证:由于 String 对象一旦创建就不能被修改,所以即使复制的是引用,也不会影响原对象
但是为啥看着这么像?
// 当你修改 String 时,实际上是创建了新的 String 对象
stu.name = "新名字"; // 这会创建一个新的 String 对象,不会影响 s1.name
效果等同深拷贝:虽然技术上是浅拷贝,但由于 String 的不可变性,效果上相当于深拷贝。 内存安全:原对象和克隆对象的 String 字段互不影响。
2.3深拷贝
深拷贝是一种更彻底的克隆方式。它创建一个新对象,并递归地复制原始对象及其所有引用类型的字段所指向的对象。
工作原理:
不仅复制对象本身,还复制对象内部所有引用链上的对象。原始对象和克隆对象是完全独立的,修改任何一个都不会影响另一个。
如何实现:
方法:递归调用 clone() 方法
这是最直接的方式。在 clone() 方法中,不仅要克隆当前对象,还要对其所有引用类型的字段也进行克隆。
代码示例:
关键点:
递归克隆 : 所有引用类型的字段都需要调用其自身的 clone() 方法 Teacher类要求 -:Teacher 类也必须实现 Cloneable 接口并重写 clone() 方法 空值检查 -:需要处理可能为 null 的引用字段
public class Stu implements Cloneable{
String name;
int age;
Teacher teacher;
public Stu(){
}
public Stu(String name,int age,Teacher teacher){
this.name=name;
this.age=age;
this.teacher=teacher;
}
@Override
public Stu clone() throws CloneNotSupportedException {
Stu stu = (Stu) super.clone();// 浅拷贝
if (this.teacher != null) {
stu.teacher = this.teacher.clone(); // 需要Teacher类也实现clone()
}
return stu;
}
}
public class Teacher implements Cloneable{
String name;
int age;
public Teacher(String name,int age)
{
this.name = name;
this.age =age;
}
@Override
public Teacher clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
Teacher t =(Teacher)super.clone();
return t;
}
}
3. 反射
定义:Java反射(Reflection)是Java语言的核心特性之一,允许程序在运行时动态获取类信息、操作类属性和方法。这种能力突破了静态编码的限制,常用于框架开发、动态代理等场景。
反射有四种方式,他们也有所区别
方式语法触发类初始化?前提条件适用场景1. 类名.classMyClass.class否编译期已知类名最安全、性能最高,用于获取编译时已知类的 Class 对象。2. 对象.getClass ()myObject.getClass()否 (类已初始化)已有该类的实例对象需要从一个实例反向获取其运行时类信息。3. Class.forName()Class.forName("com.example.MyClass")是 (默认)运行时已知类的全限定名(字符串)动态加载类,最常用的反射方式,尤其在配置文件中读取类名时。4. 类加载器.loadClass ()classLoader.loadClass("com.example.MyClass")否运行时已知类的全限定名,并能获取到相应的类加载器动态加载类,但希望延迟初始化,直到首次使用该类时才初始化。
1.通过类名.class
这是最直接、最安全的方式。编译器在编译时就会检查类是否存在,因此不会抛出 ClassNotFoundException。
特点:
不触发类初始化:这种方式只做了 “加载” 和 “链接”,不会执行类中的静态代码块 static {} 和静态变量的赋值。编译期类型安全:因为类名是硬编码的,如果类不存在,代码将无法通过编译。
// MyClass.java
public class MyClass {
static {
System.out.println("MyClass 静态代码块执行了!");
}
public MyClass() {
System.out.println("MyClass 构造函数执行了!");
}
}
// Main.java
public class Main {
public static void main(String[] args) {
System.out.println("准备通过 MyClass.class 获取 Class 对象...");
Class> clazz = MyClass.class; // 不会打印任何信息
System.out.println("成功获取 Class 对象: " + clazz.getName());
}
}
执行结果:
准备通过 MyClass.class 获取 Class 对象...
成功获取 Class 对象: com.example.MyClass
分析:可以看到,仅仅通过 MyClass.class 获取 Class 对象,并没有触发静态代码块的执行
2.通过类的加载器ClassLoader.loadClass() 加载
这种方式与 Class.forName() 类似,也是通过类的全限定名来加载类。
特点:
不触发类初始化:它只执行类的 “加载” 和 “链接”,但不会执行 “初始化” 阶段。运行时动态加载:同样非常灵活。需要处理异常:需要处理 ClassNotFoundException。可以指定类加载器:可以使用不同的类加载器来加载,这在复杂的应用(如应用服务器)中很重要。
示例:
public class MyClass {
static {
System.out.println("MyClass 静态代码块执行了!");
}
}
// Main.java
public class Main {
public static void main(String[] args) {
try {
// 获取当前类的类加载器
ClassLoader classLoader = Main.class.getClassLoader();
// 加载 MyClass,不会触发静态代码块
Class> clazz = classLoader.loadClass("com.example.MyClass");
System.out.println("成功获取 Class 对象: " + clazz.getName());
System.out.println("--- 现在首次使用 MyClass ---");
// 当我们首次主动使用这个类时(如创建实例),初始化才会发生
MyClass instance = (MyClass) clazz.newInstance();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
运行结果:
成功获取 Class 对象: com.example.MyClass
--- 现在首次使用 MyClass ---
MyClass 静态代码块执行了!
MyClass 构造函数执行了!
分析:classLoader.loadClass() 加载 MyClass 后,静态代码块并未执行。直到我们通过 clazz.newInstance() 首次尝试创建其实例时,MyClass 才被初始化,静态代码块才得以执行。
3.Class.forName()会执行类中的静态块
这是最常用的反射方式,它允许你在运行时通过一个字符串(类的全限定名)来加载类。
特点:
触发类初始化:这是它与 类加载器.loadClass() 的核心区别。默认情况下,它会执行类的静态代码块和静态变量的赋值。运行时动态加载:非常灵活,可以从配置文件、数据库或网络中读取类名来加载。需要处理异常:因为类名是字符串,编译器无法检查其有效性,所以必须处理 ClassNotFoundException。
示例:
// MyClass.java (同上)
public class MyClass {
static {
System.out.println("MyClass 静态代码块执行了!");
}
}
// Main.java
public class Main {
public static void main(String[] args) {
try {
System.out.println("准备通过 Class.forName() 获取 Class 对象...");
// 这个调用会触发静态代码块
Class> clazz = Class.forName("com.example.MyClass");
System.out.println("成功获取 Class 对象: " + clazz.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
运行结果:
准备通过 Class.forName() 获取 Class 对象...
MyClass 静态代码块执行了!
成功获取 Class 对象: com.example.MyClass
分析:Class.forName() 在加载类后,立即触发了 MyClass 的静态初始化。这也是为什么在 JDBC 中,我们常用 Class.forName("com.mysql.cj.jdbc.Driver") 来加载数据库驱动,因为驱动类的静态代码块会把自己注册到 DriverManager 中。
4.通过new 对象().getClass()
如果你已经有了一个类的实例,就可以调用它的 getClass() 方法来获取其 Class 对象。
特点:
不触发类初始化:因为对象已经被创建,说明类必然已经初始化过了。这个操作只是从对象上获取一个已经存在的 Class 对象引用。运行时动态获取:获取的是对象的实际运行时类型,这在多态场景下非常有用。
示例:
// Animal.java
class Animal {}
// Dog.java
class Dog extends Animal {}
// Main.java
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog(); // 向上转型
// getClass() 获取的是实际类型 Dog
System.out.println("myDog 的实际类型是: " + myDog.getClass().getName());
// 而 myDog 的声明类型是 Animal
System.out.println("myDog 的声明类型是: " + Animal.class.getName());
}
}
运行结果:
myDog 的实际类型是: com.example.Dog
myDog 的声明类型是: com.example.Animal
分析:getClass() 能够准确地反映对象的真实类型,即使它被向上转型为父类类型。
4.反序列化
首先,我们需要理解它的对立面:序列化 (Serialization)。
序列化:将一个内存中的对象转换成字节流(如文件、网络数据包)的过程。目的是为了将对象的状态保存下来,以便在其他时间或其他地方重建它。反序列化:将一个字节流还原成内存中的对象的过程。这就是我们今天要讨论的创造对象的方式。
反序列化主要用于以下场景:
对象持久化:将对象的状态保存到磁盘文件或数据库中,当程序下次启动时可以从文件中恢复对象,而无需重新创建和初始化。网络传输:在分布式系统中,不同服务之间(可能位于不同的物理机器上)需要传递对象。通过序列化,可以将对象转换成字节流在网络上传输,接收方再通过反序列化将其还原。缓存:将计算结果或频繁访问的数据对象序列化后存入缓存系统(如 Redis, Memcached),下次使用时直接从缓存中反序列化,避免重复计算或查询数据库。
代码示例:
import java.io.*;
import java.util.Date;
// 让 User 类实现 Serializable 接口
public class User implements Serializable {
//创建这个id是为了验证序列化和反序列化的对象Id是否一致。
private static final long serialVersionUID = 1L;
private String name;
private int age;
// transient 关键字修饰的字段不会被序列化
private transient String password;
// 创建这个字段是为了验证序列化和反序列化的时间是否一致。
private Date registrationDate;
public User(String name, int age, String password) {
System.out.println("User 类的构造函数被调用!");
this.name = name;
this.age = age;
this.password = password;
this.registrationDate = new Date();
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", password='" + password + '\'' + // 反序列化后这里会是 null
", registrationDate=" + registrationDate +
'}';
}
}
import java.io.*;
public class Main {
public static void main(String[] args) {
String filePath = "User.bin";// 文件路径
User User1 = new User("张三", 20, "123456");
System.out.println("--- 序列化之前 ---");
System.out.println(User1);
// 步骤 1: 将对象序列化到文件
File file = new File(filePath);
if (file.getParentFile() != null) {
file.getParentFile().mkdirs();
}
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath))) {
oos.writeObject(User1);// writeObject() 方法会序列化对象
System.out.println("\n对象已成功序列化到 " + filePath);
} catch (IOException e) {
e.printStackTrace();
}
//步骤 2: 从文件反序列化出对象
User User2 = null;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath))) {
// readObject() 方法会返回一个 Object,需要强制类型转换
User2 = (User) ois.readObject();
System.out.println("\n对象已成功从 " + filePath + " 反序列化");
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("\n--- 反序列化之后 ---");
// 验证反序列化出的对象
if (User2 != null) {
System.out.println(User2);
// 验证是否是一个全新的对象
System.out.println("是否为同一个对象: " + (User1 == User2)); // false
}
}
}
还有一个小贴士:
此处生成的代码的“User.bin”系统会自动生成,可以在文件中查看,不是手动创建。
反序列化创建对象的特点和原理:
从上面的例子和结果中,我们可以总结出以下几点:
不调用构造函数:
这是反序列化最核心、最独特的特点。在上面的运行结果中,你会发现 User 类的构造函数被调用! 这句话只打印了一次,那是在 new User(...) 的时候。在反序列化过程中,JVM 并没有执行 User 类的构造函数,就直接创建了一个新的 User 对象。原理:ObjectInputStream 在反序列化时,会使用一种叫做 “对象序列化机制”的内部方法。它会查找对象的类信息,然后直接从字节流中读取数据来填充对象的字段,从而绕过了常规的构造函数调用流程。这本质上也是一种深度反射 。 transient 关键字:
被 transient 修饰的字段(如 password)在序列化时会被忽略。因此,在反序列化后,这个字段会被赋予其类型的默认值(对象为 null,int 为 0 等)。这在保护敏感信息时非常有用。(我们的代码中的password,在反序列化创建对象之后打印反序列化对象出来的是null) serialVersionUID 的重要性:
serialVersionUID 是一个版本号,用于在反序列化时验证序列化对象的类版本与当前加载的类版本是否一致。如果不显式声明,JVM 会根据类的结构(字段、方法等)自动生成一个。这很危险,因为只要类结构稍有变动,自动生成的 ID 就会改变。最佳实践:无论类是否会改变,都应该显式地声明一个 private static final long serialVersionUID。serialVersionUID 是针对每一个类单独起作用,他不是JVM全局范围内唯一,而是类内唯一标识。 创建一个全新的对象:
反序列化创建的对象是一个全新的实例,它在内存中的地址与原对象不同(originalUser == deserializedUser 为 false),但它们的字段内容(除了 transient 字段)是相同的。
5. 工厂模式: 建议可以合并到new 关键字
工厂模式最终还是通过 new 来创建对象。但是,它的精髓在于将创建对象的责任从使用者手中夺走,交给一个专门的工厂来管理。这带来了解耦、灵活性和可维护性的巨大提升。new 只是一个创建对象的工具,而工厂模式是一种管理和封装创建过程的设计思想。
5.1工厂模式和直接new的区别
特性直接使用 new使用工厂模式创建逻辑分散在代码的各个角落。集中在工厂类中。耦合度业务代码与具体实现类高耦合。业务代码与具体实现类解耦,只依赖于接口。可维护性差。修改实现类需要改动多处代码。好。修改实现类只需改动工厂类。灵活性差。无法在运行时动态切换实现。好。工厂可以根据配置、参数等动态返回不同的实现。核心思想我需要一个对象,我自己亲手 new 一个。我需要一个对象,我向一个专业的 “工厂” 要一个,我不管它是怎么造出来的。
5.2用代码来区分他们
先写一个Main主体函数
public class Main {
public static void main(String[] args) {
System.out.println("=== 直接使用new关键字创建对象 ===");
// 直接使用new关键字创建对象
ConcreteProductA productA1 = new ConcreteProductA();
ConcreteProductB productB1 = new ConcreteProductB();
productA1.use();
productB1.use();
System.out.println("\n=== 使用工厂模式创建对象 ===");
// 使用工厂模式创建对象
Product productA2 = SimpleFactory.createProduct("A");
Product productB2 = SimpleFactory.createProduct("B");
productA2.use();
productB2.use();
System.out.println("\n=== 工厂模式的优势示例 ===");
// 工厂模式的优势:客户端代码不需要知道具体的产品类
String[] productTypes = {"A", "B", "A", "B"};
Product[] products = new Product[productTypes.length];
for (int i = 0; i < productTypes.length; i++) {
products[i] = SimpleFactory.createProduct(productTypes[i]);
products[i].use();
}
System.out.println("\n=== 直接new的劣势示例 ===");
// 如果不使用工厂模式,客户端代码需要知道所有的具体产品类
for (int i = 0; i < productTypes.length; i++) {
switch (productTypes[i]) {
case "A":
ConcreteProductA a = new ConcreteProductA();
a.use();
break;
case "B":
ConcreteProductB b = new ConcreteProductB();
b.use();
break;
default:
throw new IllegalArgumentException("未知的产品类型: " + productTypes[i]);
}
}
}
}
接口:
public interface Product {
void use();
}
两个产品类:
public class ConcreteProductA implements Product {
@Override
public void use() {
System.out.println("使用产品A");
}
}
public class ConcreteProductB implements Product {
@Override
public void use() {
System.out.println("使用产品B");
}
}
工厂:
public class SimpleFactory {
public static Product createProduct(String type) {
if ("A".equals(type)) {
return new ConcreteProductA();
} else if ("B".equals(type)) {
return new ConcreteProductB();
} else {
throw new IllegalArgumentException("未知的产品类型: " + type);
}
}
}
温馨小提示:
public static Product createProduct(String type)此处返回为一个接口,在下面的实现中他可以自动向上转型。
输出内容:
=== 直接使用new关键字创建对象 ===
使用产品A
使用产品B
=== 使用工厂模式创建对象 ===
使用产品A
使用产品B
=== 工厂模式的优势示例 ===
使用产品A
使用产品B
使用产品A
使用产品B
=== 直接new的劣势示例 ===
使用产品A
使用产品B
使用产品A
使用产品B
总结:由此可以看出如果我们单纯的new,遇到繁琐需要很多new创建时候会很麻烦,直接用工厂解决就更加简单易懂。