Java 面向对象进阶

Java 面向对象进阶

Java 基础

  • Java 面向对象进阶
  • 前言
  • 一、Java 数据类型
    • 1.1 基本数据类型与引用数据类型
      • 1.1.1 基本数据类型
      • 1.1.2 引用数据类型
    • 1.2 Java 自动装箱拆箱机制
  • 二、Java 面向对象
    • 2.1 类与对象
      • 2.1.1 Java 修饰符
        • 2.1.1.1 访问修饰符
        • 2.1.1.2 非访问修饰符
      • 2.1.2 抽象类与接口
        • 2.1.2.1 抽象类
        • 2.1.2.1 接口
    • 2.2 封装继承多态
      • 2.2.1 封装
      • 2.2.2 继承
      • 2.2.3 多态
        • 2.2.3.1 通过Java继承实现多态:
        • 2.2.3.2 通过Java接口实现多态:
  • 三、Java 泛型、序列化与反射机制
    • 3.1 范型
    • 3.2 序列化
    • 3.3 反射
      • 3.3.1 获取Class字节码对象
      • 3.3.2 获取构造函数
      • 3.3.3 获取字段
      • 3.3.4 获取方法
      • 3.3.5 反射的底层机制
  • 总结


前言

随着互联网的不断发展,Java这门技术也越来越重要,很多人都开启了Java学习,本文就介绍了Java的基础内容。


一、Java 数据类型

1.1 基本数据类型与引用数据类型

1.1.1 基本数据类型

  • byte:1字节(8位),数据范围是 -2^7 ~ 2^7-1 ,默认值 0
  • short:2字节(16位),数据范围是 -2^15 ~ 2^15-1 ,默认值 0
  • int:4字节(32位),数据范围是 -2^31 ~ 2^31-1 ,默认值 0
  • long:8字节(64位),数据范围是 -2^63 ~ 2^63-1 。,默认值 0L
  • float:4字节(32位),数据范围是 -3.410^38 ~ 3.410^38 。默认值 0.0F
  • double:8字节(64位),数据范围是 -1.810^308 ~ 1.810^308。默认值 0.0
  • char:2字节(16位),数据范围是 \u0000 ~ \uffff 。默认值 ‘\u0000’
  • boolean:默认值 false

1.1.2 引用数据类型

类、接口、数组都属于引用数据类型,其变量值是引用数据类型的地址值。
Student stu = new Student(); stu为Student对象的地址值。

public class Demo {
	public static void main(String[] args) {
		Student stu = new Student();
		System.out.println(stu);  
		// 输出 com.sysu.Student@5c647e05
		// com.sysu为包名,Student为类名,5c647e05为16进制地址。
	}
}

1.2 Java 自动装箱拆箱机制

Java的自动装箱和拆箱是指在基本数据类型和对应的包装类之间自动进行转换的特性。

包装类:

  • Java语言是面向对象的语言,其设计理念是“一切皆对象”。但8种基本数据类型却出现了例外,它们不具备对象的特性。正是为了解决这个问题,Java为每个基本数据类型都定义了一个对应的引用类型,这就是包装类。

自动装箱是指将基本数据类型转换为对应的包装类对象。例如,将int类型的值赋给Integer类型的变量时,会自动将int类型的值包装成Integer对象。

int num = 10;
Integer integer = num; // 自动装箱

自动拆箱是指将包装类对象转换为对应的基本数据类型。例如,将Integer类型的对象赋给int类型的变量时,会自动将Integer对象拆箱成int类型的值。

Integer integer = 10;
int num = integer; // 自动拆箱

自动装箱和拆箱可以使代码更简洁,不需要显式地进行类型转换。它们在Java编程中经常被使用,可以方便地在基本数据类型和包装类之间进行转换。

需要注意的是,自动装箱和拆箱可能会带来一些性能上的开销。在一些对性能要求较高的场景中,可以考虑手动进行装箱和拆箱操作,以避免不必要的性能损耗。

自动装箱自动拆箱原理:

  • Java的自动装箱和拆箱是通过编译器在编译时进行的。
  • 自动装箱是通过调用对应的包装类的valueOf()方法来实现的。例如,将int类型的值赋给Integer类型的变量时,编译器会自动调用Integer.valueOf()方法将int值包装成Integer对象。
  • 自动拆箱是通过调用对应的包装类的xxxValue()方法来实现的。例如,将Integer类型的对象赋给int类型的变量时,编译器会自动调用Integer.intValue()方法将Integer对象拆箱成int值。

例题:如何对Integer和Double类型判断数值相等

Integer i = 100;
Double d = 100.00;
System.out.println(i.doubleValue() == d.doubleValue());
// 不同的类无法直接进行比较,也无法调用类的compareTo方法进行比较
// 可将他们转为相同的基本数据类型进行比较

二、Java 面向对象

Java是一种面向对象的编程语言,面向对象是一种编程范式,它将程序组织成对象的集合,每个对象都有自己的属性和行为。

在Java中,一切都是对象。每个对象都是根据类定义的,类是对象的模板,描述了对象的属性和行为。通过创建类的实例(对象),可以使用对象的属性和行为来完成任务。

面向对象编程的核心概念包括:

  • 类和对象:类是对象的模板,对象是类的实例。类定义了对象的属性和行为,对象可以通过类来创建。
  • 封装:封装是将数据和操作数据的方法封装在一起,隐藏了对象的内部细节,只暴露必要的接口。通过封装,可以保护对象的数据,提高代码的可维护性和可重用性。
  • 继承:继承是一种机制,允许一个类继承另一个类的属性和方法。通过继承,可以实现代码的重用和扩展。
  • 多态:多态是指同一个方法可以根据不同的对象产生不同的行为。通过多态,可以提高代码的灵活性和可扩展性。

面向对象编程的优点包括:

  • 可重用性:通过继承和多态,可以实现代码的重用,减少重复编写代码的工作量。
  • 可维护性:通过封装和继承,可以隐藏对象的内部细节,减少对其他代码的影响,提高代码的可维护性。
  • 可扩展性:通过继承和多态,可以方便地扩展代码的功能,满足不同的需求。
  • 高效性:面向对象编程可以提高代码的执行效率和性能。

总之,Java是一种面向对象的编程语言,面向对象编程的核心概念包括类和对象、封装、继承和多态。面向对象编程可以提高代码的可重用性、可维护性、可扩展性和高效性。

2.1 类与对象

2.1.1 Java 修饰符

2.1.1.1 访问修饰符
  • public:可以被任何类访问。
  • protected:可以在同一包内的其他类、子类中访问,不同包内的子类也可以访问。
  • 默认(无修饰符):只能在同一包内访问。
  • private:只能在定义它的类内部访问。
2.1.1.2 非访问修饰符
  • static:表示静态变量或方法,不需要创建对象即可使用。
  • final:表示不可修改的常量、类或方法,不能被继承或重写。
  • abstract:用于抽象类和方法,抽象类不能直接实例化抽象方法只有声明,没有具体实现。
  • synchronized:用于同步对共享资源的访问,确保多线程的安全性。

static关键字

  • 使用static关键字修饰,则该成员由对象转为属于类,其生命周期也与类相同,该类所有对象共享该成员
  • 需要注意的是,静态方法只能访问静态变量与其他静态方法

static静态代码块

  • 静态代码块是在类加载时执行的一段代码块,它在类的所有实例对象创建之前执行,并且只执行一次。静态代码块通常用于初始化静态变量或执行一些静态方法。
  • static { // 需要执行的操作 }
  • 需要注意的是,静态代码块不能访问非静态成员变量和非静态方法,因为它们需要通过实例对象来访问。

final关键字

  • 修饰类:最终类,不能被继承
  • 修饰方法:最终方法,不能被重写
  • 修饰变量:是常量,不能被修改。(若是引用数据类型,其内部属性可修改)

2.1.2 抽象类与接口

Java中的抽象类和接口都是用于实现类的继承和实现。它们有一些共同点,但也有一些重要的区别。

共同点:

  • 都不能被实例化,只能被继承或实现。
  • 都可以包含方法的声明。
  • 都可以作为其他类的父类或实现类来使用。

区别:

  • 抽象类可以包含抽象和非抽象方法,而接口只能包含抽象方法(在Java 8之前),或者包含默认方法和静态方法(在Java 8及以后)。
  • 类只能继承一个抽象类,但可以实现多个接口
  • 抽象类可以包含成员变量,而接口只能包含常量(在Java 8之前),或者包含常量和默认方法(在Java 8及以后)。
  • 子类继承抽象类时,可以选择性地重写父类中的方法,而实现接口时,必须实现接口中的所有方法
  • 接口强调行为的规范,用于实现类的多态性和解耦,而抽象类更偏向于对一组相关的类进行抽象和统一管理

抽象类通常用于基于继承关系构建类的层次结构,提供一些通用的方法和状态,而接口则用于定义一组操作,可以被多个类实现。面向对象设计中,抽象类和接口都是很重要的概念,具体使用哪种方式取决于具体的需求和设计考虑。

2.1.2.1 抽象类

Java中的抽象类是一种特殊的类,它不能被实例化,只能被继承。抽象类用abstract关键字来声明。抽象类可以包含抽象方法和非抽象方法

抽象类的特点包括:

  • 无法实例化:抽象类不能创建对象,因为它是不完整的,必须通过子类才能实例化。
  • 可以有抽象方法:抽象方法使用abstract关键字进行声明,只有方法的声明,没有方法体。子类必须实现(重写)抽象类中的抽象方法
  • 可以有非抽象方法:抽象类也可以包含非抽象方法,这些方法可以有方法体,并且子类可以直接继承或重写这些方法。
  • 可以有构造方法:抽象类可以有构造方法,用于初始化抽象类的成员变量。
  • 可以有成员变量和常量:抽象类可以包含成员变量和常量,供子类继承和使用。
  • 可以被继承:子类通过extends关键字继承抽象类,并且必须实现抽象类中的所有抽象方法,除非子类自身也声明为抽象类。

以下是抽象类一个简单的示例:

// 定义抽象类Animal
abstract class Animal {
    // 抽象方法需要子类来实现
    public abstract void makeSound();
    
    // 非抽象方法可以有方法体
    public void sleep() {
        System.out.println("Animal is sleeping");
    }
}

// 定义Animal的子类Dog
class Dog extends Animal {
    // 实现抽象方法
    public void makeSound() {
        System.out.println("Dog is barking");
    }
}

// 定义Animal的子类Cat
class Cat extends Animal {
    // 实现抽象方法
    public void makeSound() {
        System.out.println("Cat is meowing");
    }
}

// 在主程序中使用抽象类和子类
public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog(); // 通过子类实例化对象
        dog.makeSound(); // 调用抽象方法
        dog.sleep(); // 调用非抽象方法
        
        Animal cat = new Cat(); // 通过子类实例化对象
        cat.makeSound(); // 调用抽象方法
        cat.sleep(); // 调用非抽象方法
    }
}

在上面的例子中,抽象类Animal定义了一个抽象方法makeSound()和一个非抽象方法sleep()。Dog类和Cat类是Animal类的子类,它们必须实现makeSound()抽象方法。在主程序中,我们通过子类实例化了Dog和Cat对象,并调用了它们的方法。通过抽象类的使用,我们可以将共性的行为抽象出来,提高代码的可维护性和可扩展性。

2.1.2.1 接口

接口(Interface)是一种抽象的数据类型,用于定义一组方法的规范,而不包含具体的实现。接口定义了一组方法的签名,但没有提供方法的实现细节。其他类可以通过实现接口来实现这些方法。

接口的定义使用interface关键字,语法如下:

public interface 接口名 {
    // 声明方法(可以是抽象方法、默认方法、静态方法)
}

接口的特点包括:

  • 接口中的方法都是抽象的,即没有方法体
  • 接口可以继承其他接口,使用extends关键字
  • 类通过implements关键字实现接口,然后必须实现接口中的所有方法
  • 接口中的方法默认是public和abstract的,可以省略这些修饰符
  • Java 8及以后的版本中,接口可以包含默认方法和静态方法。
  • 接口中可以定义常量,这些常量默认是public、static和final的。

接口的主要作用:

  • 实现类之间的多态性和解耦
  • 通过定义接口,可以让不同的类实现同一个接口,并提供各自的实现逻辑。这样可以让代码更灵活,可扩展性更强。
  • 接口还可以用于定义常量和规范行为,使代码更清晰和可读性更高。

以下是一个简单的接口示例:

interface Animal {
    void makeSound(); // 抽象方法
    
    // 默认方法
    default void sleep() {
        System.out.println("Animal is sleeping");
    }
    
    // 静态方法
    static void eat() {
        System.out.println("Animal is eating");
    }
}

// 实现接口
class Dog implements Animal {
    public void makeSound() {
        System.out.println("Dog is barking");
    }
}

// 使用接口
public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog(); // 通过接口实例化对象
        dog.makeSound(); // 调用抽象方法
        dog.sleep(); // 调用默认方法
        
        Animal.eat(); // 调用静态方法
    }

在上面的示例中,Animal接口定义了一个抽象方法makeSound(),一个默认方法sleep()和一个静态方法eat()。Dog类实现了Animal接口并提供了makeSound()方法的具体实现。在主程序中,我们通过接口实例化了Dog对象,并调用了接口中的方法。通过接口的使用,我们可以在不直接关联具体类的情况下,调用对象的方法并实现多态性

2.2 封装继承多态

Java中的封装、继承和多态是面向对象编程的三个核心概念。

2.2.1 封装

封装是将数据和操作数据的方法封装在一起,隐藏了对象的内部细节,只暴露必要的接口

通过封装,可以保护对象的数据提高代码的可维护性和可重用性。封装可以通过访问修饰符(如private、protected、public)来实现,将属性设置为私有的,通过公共的方法来访问和修改属性。

以下是一个简单的Java封装例子代码:

public class Person {
    private String name;
    private int age;
    private String gender;

    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 String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }
}

在这个例子中,Person类有三个私有的实例变量:name、age和gender。通过使用公共的getter和setter方法,我们可以访问和修改这些变量。例如,可以使用person.getName()来获取一个人的姓名,使用person.setAge(25)来设置一个人的年龄。

2.2.2 继承

继承是一种面向对象编程的重要概念。继承允许一个类继承另一个类的属性和方法,从而创建一个新的类。在Java中,使用关键字extends来实现继承。

下面是一个简单的Java继承的例子:

public class Animal {
    public void eat() {
        System.out.println("动物正在吃食物");
    }
}

public class Dog extends Animal {
    public void bark() {
        System.out.println("狗在叫");
    }
}

public class Cat extends Animal {
    public void bark() {
        System.out.println("猫在叫");
    }
}

在这个例子中,Animal类是一个基类,它有一个eat()方法。Dog类是一个派生类,它继承了Animal类,并且还有一个额外的bark()方法。

通过继承,Dog类可以使用Animal类的eat()方法,同时还可以添加自己的方法bark()。这样可以避免重复编写相同的代码,并且使代码更加可维护和可扩展

需要注意的是,继承的有很多缺点,所以实际开发中应该谨慎考虑是否使用继承:

  • 继承关系的紧密耦合:继承创建了一个类之间的紧密关系,子类依赖于父类的实现细节。这意味着如果父类的实现发生变化,可能会影响到所有的子类。这种紧密耦合关系可能导致代码的脆弱性和难以维护。
  • 继承层次的复杂性:当继承关系变得复杂时,继承层次可能会变得混乱和难以理解。如果继承层次过于深或过于复杂,可能会导致代码的可读性和可维护性下降。
  • 单一继承限制:在Java中,一个类只能继承自一个父类,这被称为单一继承。这意味着如果一个类需要继承多个父类的属性和方法,就无法通过继承来实现。这种限制可能会导致代码的设计和组织上的困难。
  • 继承的滥用:继承是一种强大的工具,但滥用继承可能导致代码的复杂性和不必要的耦合。如果继承关系不合理或不正确地使用,可能会导致代码的混乱和难以维护。

为了避免继承的缺点,可以考虑使用其他的面向对象编程原则和技术,如组合、接口和多态,且使用继承之前应认真构思好层次结构,如(动物-猫、狗)等。这些技术可以提供更灵活和可扩展的代码结构。

2.2.3 多态

多态允许一个对象在不同的情况下表现出不同的行为。在Java中,多态可以通过继承和接口来实现

多态的前提

  • 有继承/实现关系
  • 有父类引用指向子类对象
  • 有方法的重写

多态的优点

  • 用父类作为参数,可接收所有子类对象

多态的缺点

  • 父类对象不能使用子类对象的属性和方法

引用数据类型的转化:

  • 自动类型转换:装/拆箱,多态;
  • 强制类型转换:将父对象转化为子对象,从而使用子对象的方法。

强制类型转换检查:

Student student = new Student();
if (person instanceof Person){
	student = (Student)person;
}
2.2.3.1 通过Java继承实现多态:
public class Animal {
	public String name = "动物";
    public void makeSound() {
        System.out.println("动物发出声音");
    }
}

public class Dog extends Animal {
    public String name = "狗狗";
    public void makeSound() {
        System.out.println("狗在汪汪叫");
    }
}

public class Cat extends Animal {
    public String name = "小猫";
    public void makeSound() {
        System.out.println("猫在喵喵叫");
    }
}

public class Demo {
	public static void main(String[] args) {
	Animal animal1 = new Dog();
	Animal animal2 = new Cat();
	Dog dog = new Dog();
	Cat cat = new Cat();
	
	animal1.makeSound(); // 输出:狗在汪汪叫
	animal2.makeSound(); // 输出:猫在喵喵叫
	
	System.out.println(animal1.name) // 输出:动物
    System.out.println(animal2.name) // 输出:动物
    System.out.println(dog.name) // 输出:狗狗
    System.out.println(cat.name) // 输出:小猫
	}
}

在这个例子中,animal1和animal2对象的同一个方法表现了不同的行为,对象虽然是Animal对象,但它们分别指向了Dog类和Cat类的对象。当调用makeSound()方法时,实际上会调用派生类中重写的方法。

值得注意的是,子类并不会重写父类的属性,如上面的例子所示。

2.2.3.2 通过Java接口实现多态:

利用接口的特性实现多态性。接口定义了一组抽象方法,而实现这个接口的类必须提供方法的具体实现。通过使用接口,可以在不同的类中使用相同的方法名称,但具体的实现可能不同。

以下是一个示例代码:

interface Animal {
    void makeSound();
}

class Dog implements Animal {
    public void makeSound() {
        System.out.println("狗在汪汪叫");
    }
}

class Cat implements Animal {
    public void makeSound() {
        System.out.println("猫在喵喵叫");
    }
}

class Main {
    public static void main(String[] args) {
        Animal animal1 = new Dog();
        Animal animal2 = new Cat();

        animal1.makeSound(); // 输出:狗在汪汪叫
        animal2.makeSound(); // 输出:猫在喵喵叫
    }
}

三、Java 泛型、序列化与反射机制

3.1 范型

Java集合的设计者不知道我们会用集合来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要求具有很好的通用性。

但这样做带来如下两个问题:

  • 集合对元素类型没有任何限制,这样可能引发一些问题。例如,想创建一个只能保存Dog对象的集合,但程序也可以轻易地将Cat对象“丢”进去,所以可能引发异常。
  • 由于把对象“丢进”集合时,集合丢失了对象的状态信息,只知道它盛装的是Object,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既增加了编程的复杂度,也可能引发ClassCastException异常。

Java的范型(Generics)提供了在编译时期对数据类型进行参数化的功能,使得代码更加灵活、类型安全和可重用。范型可以应用于类、接口和方法。

范型的主要优势有:

  • 类型安全:可以在编译时检查类型的完整性,避免在运行时出现类型异常。
  • 代码重用:可以编写泛型代码,适用于多种类型(T表示接收任何类型定义),提高代码的灵活性和重用性。
  • 消除类型转换:避免了显式的类型转换,使代码更加简洁。

范型类的定义使用尖括号(<>)来指定类型参数,语法如下:

class 类名<类型参数1, 类型参数2, ...> {
  // 成员变量、方法等
}

范型接口的定义使用和范型类类似的方式:

interface 接口名<类型参数1, 类型参数2, ...> {
  // 方法定义
}

范型方法的定义使用类型参数的方式:

<类型参数> 返回类型 方法名(参数列表) {
  // 方法体
}

范型的具体使用示例如下:

class Box<T> {
    private T item;
    
    public void setItem(T item) {
        this.item = item;
    }
    
    public T getItem() {
        return item;
    }
}

public class Main {
    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<>(); // 声明一个范型类对象,指定类型参数为Integer
        integerBox.setItem(10); // 设置值
        int value = integerBox.getItem(); // 获取值
        System.out.println(value);
        
        Box<String> stringBox = new Box<>(); // 声明一个范型类对象,指定类型参数为String
        stringBox.setItem("Hello"); // 设置值
        String strValue = stringBox.getItem(); // 获取值
        System.out.println(strValue);
    }
}

"?"是通配符(Wildcard)类型,表示未知的类型。它可以用在范型的声明、方法的参数、方法的返回值等位置。通配符可以用来表示任意类型,但在使用时有一些限制。

例如,可以使用List表示一个类型未知的列表,在代码中可以读取列表的元素,但不能向列表中添加除了null之外的元素,因为不确定实际类型是否符合。

还可以使用"?"表示上界通配符或下界通配符。例如:

  • 上界通配符:List表示一个类型限定为Number或其子类的列表。
  • 下界通配符:List表示一个类型限定为Integer或其父类的列表。

"T"是类型参数(Type Parameter),用于定义泛型类、泛型接口和泛型方法中的占位符类型。它可以表示任意的引用类型,比如T表示类型参数,可以将具体的类型传递给它。

3.2 序列化

序列化(Serialization)是指将对象转换成字节流的过程,以便在网络传输或存储到文件中。而反序列化(Deserialization)则是将字节流转换回对象的过程。

序列化和反序列化的主要目的

  • 实现对象的持久化和跨网络的通信。Java提供了内置的序列化机制,可以通过实现java.io.Serializable接口来实现对象的序列化和反序列化。

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

  • 必须实现Serializable接口,并标记为可序列化。
  • 所有成员变量也必须是可序列化的,否则需要在成员变量上使用transient关键字进行标记,表示不会被序列化。

实现对象的序列化很简单,在Java中只需将对象写入到输出流中。例如:

import java.io.*;

class Student implements Serializable {
    private static final long serialVersionUID = 1L; // 序列化版本号
    private String name;
    private int age;
    
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
}

public class Main {
    public static void main(String[] args) {
        Student student = new Student("Alice", 20);
        
        // 序列化对象
        try {
            FileOutputStream fileOut = new FileOutputStream("student.ser");
            ObjectOutputStream out = new ObjectOutputStream(fileOut);
            out.writeObject(student);
            out.close();
            fileOut.close();
            System.out.println("Serialize student object successfully");
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 反序列化对象
        try {
            FileInputStream fileIn = new FileInputStream("student.ser");
            ObjectInputStream in = new ObjectInputStream(fileIn);
            Student deserializedStudent = (Student) in.readObject();
            in.close();
            fileIn.close();
            
            System.out.println("Deserialized student object:");
            System.out.println("Name: " + deserializedStudent.getName());
            System.out.println("Age: " + deserializedStudent.getAge());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

输出

Serialize student object successfully
Deserialized student object:
Name: Alice
Age: 20

上述示例中,我们定义了一个Student类,实现了Serializable接口,并标记为可序列化。然后我们创建了一个Student对象,并将其序列化到文件student.ser中。接着,我们从文件中反序列化出一个Student对象,并显示其字段值。

需要注意的是

  • 实现了Serializable接口的类中的所有非静态成员变量都将被序列化,包括私有字段。然而,静态成员变量和transient修饰的字段不会被序列化
  • 如果类中存在serialVersionUID字段,用于在反序列化期间校验版本号,以确保序列化和反序列化的兼容性。
  • 反序列化默认会创建新的对象再进行赋值,可以破坏单例模式。

3.3 反射

Java中的反射(Reflection)是指在运行时动态地获取和操作类的信息。通过反射,我们可以获取类的构造函数、字段、方法等信息,并可以在运行时动态地创建对象、调用方法、访问或修改字段的值。

反射的应用场景包括:

  • 动态创建对象
  • 调用私有方法
  • 处理注解
  • 实现通用的框架和工具等

然而,由于反射的开销较大,性能相对较低,使用时需要权衡利弊,并慎重处理异常。

值得注意的是,使用私有方法时,需要设置访问权限,可使用下面的函数进行访问权限控制。public void setAccessible(boolean flag)默认false,设置为true则获取访问权限。

常见的反射操作:

3.3.1 获取Class字节码对象

  1. 使用Class.forName(String PakageAndClassName)方法
  2. 通过类的对象的getClass()方法
  3. 使用.class语法获取

以下的是简单示例:

Class<?> clazz = Class.forName("com.example.MyClass");
Class<?> clazz = obj.getClass();
Class<?> clazz = MyClass.class;

3.3.2 获取构造函数

可以使用Class对象的getConstructors()方法获取所有公共构造函数,getDeclaredConstructors()方法获取所有构造函数(包括私有的),还可以根据参数类型获取特定的构造函数。通过构造函数可以实例化对象

例如:

Constructor<?>[] constructors = clazz.getConstructors();
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
Object obj = constructor.newInstance("example", 100);

3.3.3 获取字段

可以使用Class对象的getFields()方法获取所有公共字段,getDeclaredFields()方法获取所有字段(包括私有的),还可以根据字段名获取特定字段。通过字段可以获取或设置对象的属性值。

例如:

Field[] fields = clazz.getFields();
Field[] fields = clazz.getDeclaredFields();
Field field = clazz.getField("fieldName");
Object value = field.get(obj);
field.set(obj, newValue);

3.3.4 获取方法

可以使用Class对象的getMethods()方法获取所有公共方法,getDeclaredMethods()方法获取所有方法,还可以根据方法名和参数类型获取特定方法。通过方法可以调用对象的方法。

示例:

Method[] methods = clazz.getMethods();
Method[] methods = clazz.getDeclaredMethods();
Method method = clazz.getMethod("methodName", int.class);
Object result = method.invoke(obj, 100);

3.3.5 反射的底层机制

反射的底层机制涉及到Java虚拟机(JVM)的类加载、字节码解析和内存布局等方面。下面是反射的主要底层机制:

  1. 类加载:在Java中,类的加载是通过类加载器(ClassLoader)完成的。类加载器负责将类的字节码文件加载到内存中,并生成对应的Class对象。通过反射获取Class对象时,实际上是通过类加载器从类路径或其他位置加载类的字节码文件,然后生成Class对象。
  2. 获取类的字节码:通过类加载器加载类的字节码文件后,JVM会将字节码转换为对应的Class对象。Class对象包含了类的结构信息,包括类的构造函数、字段、方法等。
  3. 字节码解析:当获取到Class对象后,可以使用反射来解析字节码,提取类的结构信息。通过解析字节码,可以获取类的成员变量、方法信息以及注解等。这样,我们就可以动态地了解和操作类的结构。
  4. 内存布局:在内存中,Java虚拟机为每个类的实例都分配一定的内存空间。反射机制能够通过Class对象获取到类的字段,然后通过字段的偏移量,在内存中准确地读写字段的值。
  5. 方法调用:通过反射,可以动态调用类的方法。通过获取到方法的引用,然后通过Method对象来调用方法。底层机制会根据方法的字节码进行调用,实现动态的方法调用。

总结起来,反射的底层机制主要涉及类加载、字节码解析、内存布局和动态方法调用等方面,通过这些机制可以在运行时动态地获取和操作类的信息。

总结

Tring to do better.

你可能感兴趣的:(java,开发语言)