Java 类(Class)是面向对象编程(OOP)的基础构建块。一个类定义了一组具有相同属性(字段)和方法的对象。
类的定义
一个 Java 类是通过 class
关键字来定义的,其基本结构如下:
public class ClassName {
// 类的成员变量(字段)
// 类的构造器
// 类的方法
}
类的组成部分
成员变量
成员变量是类中定义的数据项,它们表示对象的属性或状态。成员变量可以是以下几种类型:
- 基本数据类型:如
int
、double
、float
、boolean
、char
等。 - 引用数据类型:如
String
、数组、类类型(自定义类)、接口类型等。
成员变量的声明通常包括访问修饰符、类型和变量名。以下是成员变量的几个特点:
- 初始化:成员变量在类实例化时会被初始化,基本类型有默认值(如
int
默认为 0),引用类型默认为null
。 - 作用域:成员变量的作用域是整个类,但它们的访问可能受到访问修饰符的限制。
- 封装:通常建议将成员变量设置为
private
,并通过公共方法(getter 和 setter)来访问和修改它们。
public class Person {
private String name; // 私有成员变量,表示人的名字
private int age; // 私有成员变量,表示人的年龄
}
构造器
构造器是类中的一种特殊方法,用于创建并初始化类的新对象。以下是构造器的几个特点:
- 名称:构造器的名称必须与类名完全相同。
- 无返回类型:构造器没有返回类型,也不可以使用
void
。 - 初始化:构造器用于给成员变量赋初始值。
- 重载:类可以有多个构造器,只要它们的参数列表不同。
构造器可以接受参数,这些参数用于初始化对象的成员变量。
查看代码
public class Person {
private String name;
private int age;
// 构造器,接受两个参数用于初始化成员变量
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 无参构造器,可以提供默认值或进行其他初始化
public Person() {
this.name = "Unknown";
this.age = 0;
}
}
方法
方法是类中定义的行为,它们包含一系列执行特定任务的语句。以下是方法的几个特点:
- 访问修饰符:方法可以有访问修饰符,控制其可见性。
- 返回类型:方法可以返回一个值,该值类型必须与方法声明中的返回类型相匹配。
- 参数:方法可以接受任意数量的参数,参数类型和数量在方法声明中指定。
- 重载:类中可以有多个同名方法,只要它们的参数列表不同。
this
关键字:在方法内部,this
关键字可以用来引用当前对象。
查看代码
public class Person {
// ...
// 方法,没有返回值(void),打印个人信息
public void introduce() {
System.out.println("My name is " + name + " and I am " + age + " years old.");
}
// 方法,返回类型为 String,获取名字
public String getName() {
return name;
}
}
访问修饰符
Java 提供了四种访问修饰符,用于控制类成员的可见性和访问级别:
public
:成员可以被任何类访问,无论它们是否在同一个包中。private
:成员只能在声明它们的类内部访问。protected
:成员可以在同一个包内的任何类中访问,以及在其他包中的子类中访问。- 默认(没有修饰符):成员可以在同一个包内的任何类中访问,但不能被其他包中的类访问。
// 在当前包中
package cengxuyuanPackage;
public class Person {
public String publicName; // 可以被任何类访问
private int privateAge; // 只能在 Person 类内部访问
protected boolean isAdult; // 可以在同一个包内或子类中访问
int defaultNumber; // 默认访问修饰符,只能在同一个包内访问
}
public
当一个成员被声明为public
时,它可以被任何其他类访问,无论这些类是否在同一个包内。
查看代码
// 在另一个包中的任意类
package anotherPackage;
import cengxuyuanPackage.Person; // 假设 Person 类位于 cengxuyuanPackage 包中
public class AnotherClass {
public void printName(Person person) {
System.out.println(person.publicName); // 可以访问 public 成员
}
}
private
当一个成员被声明为private
时,它只能在其定义的类内部被访问。这通常用于隐藏数据细节,提供封装。
查看代码
public class Person {
private int privateAge;
public int getAge() {
return privateAge; // 在 Person 类内部可以访问 private 成员
}
public void setAge(int age) {
if (age > 0) { // 可以添加逻辑验证
this.privateAge = age;
}
}
}
// 尝试从外部直接访问 privateAge 会导致编译错误
// Person p = new Person();
// p.privateAge = 30; // 错误: privateAge 具有 private 访问权限
protected
protected
成员可以在同一包内的所有类以及不同包中的子类中访问。这使得子类能够访问或覆盖父类的某些功能。
查看代码
// 在另一个包中的子类
package anotherPackage;
import cengxuyuanPackage.Person; // 假设 Person 类位于 cengxuyuanPackage 包中
public class Employee extends Person {
public void checkIsAdult() {
if (isAdult) { // 子类可以访问 protected 成员
System.out.println("This is an adult.");
}
}
}
默认
如果一个成员没有指定任何访问修饰符,那么它的默认访问级别就是包私有的。这意味着只有在同一包内的类才能访问这个成员。
查看代码
// 在同一个包中的另一个类
package cengxuyuanPackage;
public class Tester {
public void testDefaultNumber(Person person) {
int number = person.defaultNumber; // 同一包内可以访问 default 成员
System.out.println(number);
}
}
// 如果 Tester 类位于不同的包中,则无法访问 defaultNumber
静态成员
静态成员属于类本身,而不是类的任何实例。以下是静态成员的几个特点:
- 访问:静态成员可以通过类名直接访问,无需创建类的实例。
- 初始化:静态成员在类加载时被初始化,只初始化一次。
- 共享:静态成员被类的所有实例共享。
静态成员可以是静态变量(字段)或静态方法。
查看代码
public class MathUtils {
// 静态方法,可以直接通过类名调用
public static int add(inta, int b) {
return a + b;
}
// 静态变量,属于类,可以通过类名访问
public static final int MAX_VALUE = 100;
}
// 调用静态方法
int result = MathUtils.add(5, 10);
// 访问静态变量
int maxValue = MathUtils.MAX_VALUE;
静态方法通常用于执行与类相关的操作,而不是与类的特定实例相关的操作。静态变量通常用于存储类级别的常量或共享状态。
静态方法
- 定义:静态方法通过在方法前添加
static
关键字来定义。 - 调用:静态方法可以通过类名直接调用,无需创建类的实例。
- 访问成员:静态方法只能直接访问静态成员变量和静态方法。如果需要访问非静态成员,必须通过类的实例来访问。
public class Utility {
public static int multiply(int a, int b) {
return a * b;
}
}
// 调用静态方法
int product = Utility.multiply(3, 4);
静态变量
- 定义:静态变量通过在变量前添加
static
关键字来定义。 - 初始化:静态变量在类加载时进行初始化,并且只初始化一次。
- 共享状态:静态变量是所有类实例共享的,因此任何对静态变量的修改都会影响到所有实例。
查看代码
public class Counter {
public static int count = 0; // 静态变量,用于计数
public Counter() {
count++; // 每次创建实例时,计数器增加
}
}
// 使用静态变量
Counter c1 = new Counter();
Counter c2 = new Counter();
System.out.println(Counter.count); // 输出 2,因为创建了两个实例
- 静态方法与非静态方法:静态方法不能直接访问非静态成员变量或方法,除非通过类的实例来访问。
- 静态初始化:静态成员的初始化发生在类加载时,而不是实例化时。
- 静态变量与实例变量:静态变量属于类,实例变量属于类的实例。
类的实例化
类的实例化是指创建类的对象的过程。每个对象都是其类的实例,具有该类的所有成员变量和方法。
创建类的实例(对象)通过 new
关键字和构造器来完成。
当使用 new
关键字创建对象时,构造器会被自动调用。构造器的作用是初始化新创建的对象的状态。
Person person = new Person("Alice", 30);
类的继承
Java 支持继承,允许一个类继承另一个类的字段和方法。子类可以使用 extends
关键字来继承父类。
public class Student extends Person {
// ...
}
类的接口
接口定义了一组抽象方法,任何实现接口的类都必须提供这些方法的具体实现。
public interface Animal {
void sound();
}
public class Dog implements Animal {
public void sound() {
System.out.println("Bark");
}
}
类的多态
多态是面向对象编程(OOP)中的一个核心概念,它允许我们使用一个共同的接口来代表不同的实现方式,从而使得程序更加灵活和可扩展。
多态的类型
多态可以分为两种类型:编译时多态(静态多态)和运行时多态(动态多态)。
编译时多态
编译时多态主要是指方法的重载,它根据参数列表的不同来区分不同的方法。这种多态在编译阶段就已经确定,因此称为静态多态。
运行时多态
运行时多态则是动态的,通过动态绑定来实现。它依赖于继承和接口实现机制,允许父类引用或接口引用指向不同的子类实例。这种多态性在程序运行时才确定下来,因此称为动态多态。
在 Java 中,运行时多态可以通过继承和接口实现来实现。
多态的原理
多态的原理依赖于Java的运行时类型识别(RTTI)。每个Java对象都有一个对应的Class对象,它包含了对象的类型信息。即使在向上转型后,对象的Class对象仍然是其实际类型。
Java虚拟机(JVM)中的方法调用是通过方法表来实现的。方法表是Class对象的一部分,它记录了类中所有方法的引用。当子类重写父类的方法时,方法表中的引用会被更新,指向子类的方法。
多态方法调用
多态的实现主要依赖于两种不同的字节码指令:invokevirtual
和 invokeinterface
。
invokevirtual 指令
invokevirtual
指令是JVM中用于调用实例方法的指令。当JVM执行这个指令时,它会按照以下步骤进行方法调用:
- 确定接收者对象:指令执行前,操作数栈顶部的元素必须是实例方法的接收者对象,也就是该方法所属的类的实例。
- 查找方法表:JVM会根据接收者对象的实际类型(在运行时确定),查找该类型的方法表。方法表是类信息的一部分,存储在方法区中,它包含了类的所有方法引用。
- 使用固定偏移量:由于每个方法在方法表中的位置是固定的,JVM可以使用一个索引(通常是方法表中方法的偏移量)来直接定位到要调用的方法。
- 动态绑定:即使
invokevirtual
指令在编译时已经确定了要调用的方法签名,实际的调用目标仍然是在运行时确定的。这是因为invokevirtual
指令支持多态,即子类可以重写父类的方法。如果子类重写了该方法,方法表中相应的条目将指向子类的方法实现。 - 方法调用:一旦找到方法引用,JVM就会执行该方法。
invokeinterface 指令
invokeinterface
指令用于调用接口方法。由于接口方法可能由多个类实现,因此JVM在执行这个指令时采取了不同的策略:
- 确定接收者对象:与
invokevirtual
相同,操作数栈顶部的元素是接口方法的接收者对象。 - 查找接口方法表:JVM需要查找实现该接口的接收者对象的实际类型的接口方法表。接口方法表不同于类的方法表,它不包含方法的实现,而是包含指向实现该方法的具体类的引用。
- 搜索方法表:由于接口可以由多个类实现,JVM不能使用固定偏移量来查找方法。因此,JVM必须搜索接口方法表来找到与调用签名匹配的方法。
- 动态绑定:与
invokevirtual
一样,invokeinterface
指令也支持多态,允许不同的实现类有不同的方法实现。 - 方法调用:找到方法实现后,JVM执行该方法。
总结
对于类的方法调用,JVM使用固定的偏移量在方法表中查找方法。
而对于接口的方法调用,由于接口可以实现多个,JVM需要搜索整个方法表来找到正确的方法。
由于接口的方法调用需要搜索方法表,因此性能上通常会比类的方法调用要慢。这也提醒我们,在设计时,不应该盲目地优先选择接口。
多态的实现
多态的实现基于两个关键概念:继承和方法重写。
继承
多态的基础是继承。子类继承自父类,并可以覆盖父类的方法。这样,子类对象就可以替换父类对象,因为它们共享相同的接口(父类的方法签名)。
方法重写
方法重写(也称为方法覆盖)是多态的一个关键特性。子类可以提供与父类方法相同的签名,这意味着子类的方法可以替换父类的方法。当调用一个方法时,Java 虚拟机(JVM)会根据实际的对象类型来决定调用哪个方法。
在 Java 中,多态的实现涉及以下步骤:
- 创建父类和子类:首先定义一个父类,然后创建一个或多个子类。
- 继承和重写:子类继承自父类,并重写父类的方法。
- 多态引用:创建子类的对象,并将其赋值给父类的引用。
- 方法调用:通过父类的引用调用方法,Java 虚拟机会根据实际对象类型来决定调用哪个方法。
假设我们有一个 Animal
类和一个 Dog
类,Dog
类继承自 Animal
类并重写了 makeSound
方法。
查看代码
public class Animal {
public void makeSound() {
System.out.println("The animal makes a sound.");
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("The dog barks.");
}
}
现在,我们可以创建一个 Dog
对象,并将其赋值给一个 Animal
类型的引用。
Animal animal = new Dog();
当我们通过 animal
引用调用 makeSound
方法时,Java 虚拟机会调用 Dog
类中的重写方法。
animal.makeSound(); // 输出 "The dog barks."
内部类
内部类是指在一个类内部定义的类。它可以访问外部类的所有成员,包括私有成员。
非静态内部类
非静态内部类是依赖于外部类实例的。这意味着,要创建非静态内部类的实例,必须首先创建外部类的实例。
查看代码
public class OuterClass {
private int outerField;
class InnerClass {
void accessOuterField() {
System.out.println(outerField); // 访问外部类的成员
}
}
}
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
inner.accessOuterField();
静态内部类
静态内部类是一个静态类,它不依赖于外部类的实例。它可以直接通过类名访问,不需要外部类的实例。
查看代码
public class OuterClass {
private static int outerStaticField;
static class InnerStaticClass {
void accessOuterStaticField() {
System.out.println(outerStaticField); // 访问外部类的静态成员
}
}
}
OuterClass.InnerStaticClass innerStatic = new OuterClass.InnerStaticClass();
innerStatic.accessOuterStaticField();
内部类和静态内部类的区别
对外部类实例的依赖
- 内部类(非静态内部类):需要依赖于外部类的实例。在内部类中,可以访问外部类的所有成员(包括非静态成员和静态成员)。
- 静态内部类:不需要依赖于外部类的实例。静态内部类只能直接访问外部类的静态成员。
创建实例的方式
- 内部类(非静态内部类):创建实例时,需要通过外部类的实例来创建。例如:java
OuterClass outer = new OuterClass(); OuterClass.InnerClass inner = outer.new InnerClass();
- 静态内部类:创建实例时,不需要外部类的实例。可以直接通过类名来创建。例如:java
OuterClass.StaticNestedClass staticNested = new OuterClass.StaticNestedClass();
成员变量和方法
- 内部类(非静态内部类):可以包含非静态成员和静态成员,但通常不推荐在内部类中定义静态成员,因为这样会使得内部类与外部类的实例关系变得模糊。
- 静态内部类:可以包含静态成员和非静态成员,但是非静态成员不能直接访问外部类的非静态成员。
访问权限
- 内部类(非静态内部类):可以访问外部类的所有成员(包括私有成员)。
- 静态内部类:只能直接访问外部类的静态成员和静态内部类。如果需要访问外部类的非静态成员,必须通过外部类的实例来访问。
作用域
- 内部类(非静态内部类):通常只在非静态方法或者非静态代码块中创建和使用。
- 静态内部类:可以在任何地方使用,因为它不依赖于外部类的实例。
枚举
Java中的枚举(Enum)是一种特殊的类,它允许定义一组常量。
枚举的声明
首先,我们通过enum
关键字来定义一个枚举类型。例如,定义一个星期天数的枚举如下:
public enum Day {
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY;
}
这里的Day
是一个枚举类型,而SUNDAY
, MONDAY
等是这个枚举类型的实例。
编译器如何处理枚举
当我们编译包含枚举的Java文件时,编译器会自动生成一些额外的代码来支持枚举的功能。对于上面的例子,编译器实际上生成了类似于以下的代码:
- 继承自
java.lang.Enum
:每个枚举类型都是从java.lang.Enum
类隐式继承的,这意味着枚举类型不能直接或间接地继承其他类。 - 私有构造函数:编译器为枚举类型提供了一个私有的构造函数。因此,我们无法在外部创建枚举类型的实例。
- 静态final字段:每个枚举常量都变成枚举类型内的一个静态final字段。这些字段的类型就是该枚举类型本身。
- values()方法:编译器添加了一个名为
values()
的方法,返回一个包含所有枚举常量的数组。 - valueOf(String name)方法:还有一个
valueOf(String name)
方法,用于根据给定的名字返回对应的枚举常量。如果名字不匹配任何枚举常量,则抛出IllegalArgumentException
。
java.lang.Enum
类
java.lang.Enum
类是Java中所有枚举类型的公共基类。它是一个抽象类,由编译器自动生成的枚举类型隐式继承。这意味着当你定义一个枚举时,实际上是在创建一个继承自Enum
的新类。Enum
类提供了几个关键的方法和属性,支持枚举值的各种操作。
重要方法
方法签名 | 描述 |
---|---|
protected Enum(String name, int ordinal) | 构造函数,用于初始化枚举常量。参数包括枚举常量的名字和位置索引。不允许从类外部直接调用。 |
String name() | 返回此枚举常量的名称。 |
int ordinal() | 返回枚举常量的位置索引,基于其声明的顺序,从 0 开始。 |
static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) | 根据给定的枚举类型和名字返回对应的枚举常量。如果指定的名字不存在,则抛出IllegalArgumentException 。 |
Class<E> getDeclaringClass() | 返回表示此枚举常量所属的枚举类型的Class 对象。 |
boolean equals(Object other) | 检查当前枚举常量是否等于另一个对象。只有当两个引用指向同一个枚举常量时才相等。 |
int compareTo(E o) | 允许枚举常量之间进行比较。比较基于枚举常量的序号。 |
<T extends Enum<T>> T[] values() | 返回包含枚举类型所有元素的数组。 |
初始化
在类加载时,JVM 首先分配内存给类的静态字段,并执行静态初始化块。对于枚举类型,这意味着枚举常量将被创建并分配内存。
每个枚举常量都是通过调用 Enum
类的受保护构造器来实例化的,传递常量的名称和它在枚举声明中的序号。
一旦静态初始化块执行完毕,枚举类型的初始化就完成了。此后,每次访问枚举常量时,都会直接返回已经存在的单例对象。
EnumMap
和 EnumSet
EnumMap
和 EnumSet
是 Java 集合框架中专门为枚举类型设计的两个类,它们都位于 java.util
包中。这两个类利用了枚举类型的一些特性(如固定数量的实例、有序性等)来提供高效的实现。
EnumMap
EnumMap
是一个专门为枚举键设计的映射实现。它是一个有序的映射,其键必须来自单个枚举类型。EnumMap
在内部使用数组来表示,每个枚举键直接映射到数组的一个索引,这使得它非常高效。
特点:
- 键必须是单个枚举类型的枚举常量。
- 它不是线程安全的。
- 它允许包含 null 值,但不允许包含 null 键。
- 它比基于
HashMap
的实现更高效,因为它不需要计算哈希码,并且数组访问速度非常快。
示例:
查看代码
import java.util.EnumMap;
import java.util.Map;
public class EnumMapExample {
enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE };
public static void main(String[] args) {
EnumMap<Size, String> sizes = new EnumMap<>(Size.class);
sizes.put(Size.SMALL, "S");
sizes.put(Size.MEDIUM, "M");
sizes.put(Size.LARGE, "L");
sizes.put(Size.EXTRA_LARGE, "XL");
for (Map.Entry<Size, String> entry : sizes.entrySet()) {
System.out.println(entry.getKey() + " = " + entry.getValue());
}
}
}
EnumSet
EnumSet
是一个专门为枚举类型设计的集合实现。它是一个集合,其元素必须是单个枚举类型的枚举常量。EnumSet
在内部使用位向量实现,这使得它非常高效,特别是对于操作如添加、删除和包含测试。
特点:
- 元素必须是单个枚举类型的枚举常量。
- 它不是线程安全的。
- 它不允许包含 null 元素。
- 它比基于
HashSet
的实现更高效,因为它使用了位操作。
示例:
查看代码
import java.util.EnumSet;
import java.util.Set;
public class EnumSetExample {
enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE };
public static void main(String[] args) {
Set<Size> sizes = EnumSet.of(Size.SMALL, Size.LARGE);
sizes.add(Size.EXTRA_LARGE);
for (Size size : sizes) {
System.out.println(size);
}
}
}
参考链接: