What is Java ?(二)
What is Java ?(二)
xiaoyan内容:拷贝
、泛型
、对象
、反射
本篇博客是笔者作为初学者记录自己对Java一些基本概念的理解。内容参考了大量网络资源,篇幅很长,旨在作为个人学习笔记,供自己日后回顾和复习。
拷贝
浅拷贝和深拷贝的区别
浅拷贝(Shallow Copy)
浅拷贝复制了一个对实例对象的引用,因此在内存中这两个对象指向的是同一个对象。换句话说,浅拷贝只复制了对象的引用,而不是对象本身。
示例代码
1 | class ShallowCopyExample { |
解释:original
和 copy
指向同一个对象,因此修改 copy
的值也会影响 original
。
深拷贝(Deep Copy)
深拷贝新建一个一模一样的实例对象,两个对象的引用指向的是不同的地址。换句话说,深拷贝复制了对象本身,而不是对象的引用。
示例代码
1 | class DeepCopyExample implements Cloneable { |
解释:original
和 copy
指向不同的对象,因此修改 copy
的值不会影响 original
。
图示
实现深拷贝的方法
常用的深拷贝方法有三种:
- 实现
Cloneable
接口的clone
方法 - 使用序列化和反序列化
- 手写递归复制
1. 实现 Cloneable
接口的 clone
方法
通过实现 Cloneable
接口并重写 clone
方法,可以实现深拷贝。需要注意的是,clone
方法默认是浅拷贝,因此需要手动处理对象的深拷贝。
示例代码
1 | class DeepCopyExample implements Cloneable { |
2. 使用序列化和反序列化
通过将对象序列化为字节流,然后再反序列化为新的对象,可以实现深拷贝。这种方法适用于实现了 Serializable
接口的对象。
示例代码
1 | import java.io.*; |
3. 手写递归复制
通过手写递归方法,逐层复制对象的每个字段,可以实现深拷贝。这种方法适用于任何对象,但需要手动处理每个字段的复制。
示例代码
1 | import java.util.ArrayList; |
泛型
什么是泛型?
泛型是 Java 语言中的一个重要特性,它允许类、接口和方法在定义时使用一个或多个类型参数,而这些参数在实际运行时才会指定具体的参数类型。泛型提供了一种在编译时进行类型检查的机制,从而提高代码的类型安全性和可读性。
为什么需要泛型?
泛型的主要目的是提高代码的类型安全性和可读性,具体体现在以下几个方面:
适用于多种类型执行相同的代码:
- 泛型允许在定义类、接口和方法时使用类型参数,从而可以在不同的类型上执行相同的代码。
类型安全:
- 泛型在编译时进行类型检查,确保类型的一致性,避免运行时类型转换错误。
- 使用泛型可以减少强制类型转换的需求,提高代码的可读性和安全性。
示例代码
1 | import java.util.ArrayList; |
通过这些特性,泛型在 Java 编程中提供了更强大的类型检查和代码复用能力,使得代码更加健壮和易于维护。
对象
创建对象的方式
使用
new
关键字:这是最常见和最直接的对象创建方式。通过调用类的构造函数来实例化对象。
示例:
1
MyClass obj = new MyClass();
使用
clone
方法:通过调用对象的
clone
方法来创建对象的副本。需要注意的是,类必须实现Cloneable
接口。示例:
1
MyClass obj2 = (MyClass) obj.clone();
使用反序列化:
通过将对象序列化为字节流,然后再反序列化来创建对象。这通常用于对象的持久化存储和传输。
示例:
1
2
3
4
5try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.obj"))) {
MyClass obj3 = (MyClass) in.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}使用
Class
类的newInstance
方法:通过调用
Class
类的newInstance
方法来创建对象。需要注意的是,该方法要求类具有无参构造函数。示例:
1
2
3
4
5try {
MyClass obj4 = MyClass.class.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}使用
Constructor
的newInstance
方法:通过调用
Constructor
类的newInstance
方法来创建对象。这种方式允许使用带参数的构造函数。示例:
1
2
3
4
5
6try {
Constructor<MyClass> constructor = MyClass.class.getConstructor(String.class);
MyClass obj5 = constructor.newInstance("exampleParameter");
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
new
的对象什么时候回收?
通过关键字new
创建的对象,由Java的垃圾回收器(Garbage Collector, GC)负责回收。垃圾回收器的工作是在程序运行过程中自动进行的,它会周期性地检测不再被引用的对象,并将其回收以释放内存。
回收时机
Java对象的回收时机是由垃圾回收器根据一些算法来决定的,主要有以下几种情况:
- 引用计数法:当某个对象的引用计数为0时,表示该对象不再被引用,可以被回收。然而,Java并不采用引用计数法,因为它无法解决循环引用的问题。
- 可达性分析算法:从根对象(如方法区中的类静态属性、方法中的局部变量等)出发,通过对象之间的引用链进行遍历。如果存在一条引用链到达某个对象,则说明该对象是可达的;反之,不可达的对象将被回收。可达性分析算法是Java垃圾回收器主要采用的方法。
- 终结器(Finalizer):如果对象重写了
finalize()
方法,垃圾回收器会在回收该对象之前调用finalize()
方法。对象可以在finalize()
方法中进行一些清理操作。然而,终结器机制的使用不被推荐,因为它的执行时间是不确定的,可能会导致不可预测的性能问题。
流程如下:
反射
什么是反射?
Java反射机制是指在运行状态中,任意一个类都能知道这个类中的所有方法和属性,并且任意对象都能调用这个类对象的属性和方法。这种动态获取的信息和调用对象方法的功能称为Java的反射机制。
反射机制如图所示:
动态性:
- 反射允许在运行时动态地获取类的完整结构信息,包括类名、父类、方法和属性等,并调用其方法(包括私有方法),而不需要在编译时确定。
- 示例:通过
Class.forName()
方法动态加载类,并使用getMethods()
、getFields()
等方法获取类的结构信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import java.lang.reflect.Method;
import java.lang.reflect.Field;
public class DynamicReflectionExample {
public static void main(String[] args) {
try {
// 动态加载类
Class<?> clazz = Class.forName("com.example.MyClass");
// 获取类的所有方法
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println("Method: " + method.getName());
}
// 获取类的所有字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println("Field: " + field.getName());
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}灵活性:
- 反射提供了灵活的编程方式,可以在运行时根据需要创建对象、调用方法和访问属性。
- 示例:使用反射API动态地创建对象实例,即使在编译时不知道具体的类名。这是通过
Class
类的newInstance()
方法或Constructor
对象的newInstance()
方法实现的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import java.lang.reflect.Constructor;
public class FlexibleReflectionExample {
public static void main(String[] args) {
try {
// 动态加载类
Class<?> clazz = Class.forName("com.example.MyClass");
// 使用无参构造函数创建对象实例
Object obj = clazz.getDeclaredConstructor().newInstance();
// 使用带参构造函数创建对象实例
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
Object objWithParam = constructor.newInstance("Parameter");
} catch (Exception e) {
e.printStackTrace();
}
}
}访问私有成员:
- 反射可以访问类的私有成员(如私有方法和私有属性),这在正常情况下是不允许的。
- 示例:通过
Field
类的setAccessible(true)
方法绕过访问控制,使用get()
和set()
方法访问和修改私有字段的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class PrivateAccessReflectionExample {
public static void main(String[] args) {
try {
// 动态加载类
Class<?> clazz = Class.forName("com.example.MyClass");
// 创建对象实例
Object obj = clazz.getDeclaredConstructor().newInstance();
// 访问私有字段
Field privateField = clazz.getDeclaredField("privateField");
privateField.setAccessible(true);
privateField.set(obj, "New Value");
System.out.println("Private Field Value: " + privateField.get(obj));
// 调用私有方法
Method privateMethod = clazz.getDeclaredMethod("privateMethod");
privateMethod.setAccessible(true);
privateMethod.invoke(obj);
} catch (Exception e) {
e.printStackTrace();
}
}
}
反射常见应用场景
框架开发:许多框架(如Spring、Hibernate)使用反射来动态加载和配置类,实现依赖注入和AOP(面向切面编程)等功能。
序列化和反序列化:在对象的序列化和反序列化过程中,反射用于动态地访问和设置对象的属性。
动态代理:反射与动态代理结合使用,可以在运行时创建代理对象,实现方法的拦截和增强。
单元测试:单元测试框架(如JUnit)使用反射来动态地发现和执行测试方法。
插件化系统:反射用于动态加载和执行插件,使得系统具有扩展性和灵活性。
ORM框架:ORM(对象关系映射)框架使用反射来将数据库表映射到Java对象,并动态地生成SQL语句。
反射的优缺点
优点
- 灵活性:反射提供了极大的灵活性,允许在运行时动态地操作类和对象。
- 扩展性:反射使得系统具有更好的扩展性,可以通过插件或配置文件动态加载和执行代码。
缺点
- 性能开销:反射操作通常比直接调用方法或访问属性要慢,因为它涉及到动态解析和安全检查。
- 安全风险:反射可以访问和修改类的私有成员,这可能会导致安全问题。
- 代码可读性:反射代码通常比直接调用方法或访问属性的代码更复杂,可读性较差。