面向对象
概念
面向对象编程(Object-oriented Programming,OOP)是一种广泛应用于软件开发的编程范式。它通过将数据和对数据操作的方法封装在一个独立的实体中,即对象,来组织和管理代码。面向对象编程强调在编程过程中模拟真实世界中的实体和其相互关系。
定义类
我们需要搞清楚几件事情:对象是根据类创建来的,类是根据现实生活中的事物抽象而来的。
把事物的属性定义为类中的成员变量。
把事物的行为(功能)定义为类中的成员变量。
三个基本特征
Java面向对象的三个基本特征是封装、继承和多态,这三个基本特征是面向对象编程的核心。
权限修饰符
概述
在Java中提供了四种访问权限,使用不同的访问权限修饰符修饰时,被修饰的内容会有不同的访问权限,
- public:公共的
- protected:受保护的
- 不写:默认的
- private:私有的
不同权限的访问能力
public | protected | default(空的) | private | |
---|---|---|---|---|
同一类中 | √ | √ | √ | √ |
同一包中(子类与无关类) | √ | √ | √ | |
不同包的子类 | √ | √ | ||
不同包中的无关类 | √ |
可见,public具有最大权限。private则是最小权限。
编写代码时,如果没有特殊的考虑,建议这样使用权限:
- 成员变量使用 private,隐藏细节(封装)。
- 构造方法使用 public,方便创建对象。
- 成员方法使用 public,方便调用方法。
- 类的定义可以使用 public,默认权限,但是建议使用public权限。
方法
方法的概念和作用
方法的概念是基于面向对象编程的思想,它使得代码可以更加模块化和可重复使用。通过将代码逻辑封装在方法中,我们可以将复杂的问题分解成多个简单的任务,并且可以在多个地方重复调用这些方法。
通过使用方法,我们可以使代码更加清晰、简洁和易于维护。我们可以将不同的任务分别封装在不同的方法中,使得代码的逻辑更加清晰。方法还可以提高代码的复用性,当我们需要执行相同或类似的任务时,只需要调用已经定义好的方法即可,而不需要重复编写相同的代码。
总的来说,Java方法的作用是提高代码的可读性、可维护性和可重用性。它是面向对象编程的重要组成部分,帮助我们构建更好的软件系统。
定义方法的语法
访问修饰符 返回类型/void 方法名(参数列表[形参]) {
// 方法体
// 执行特定任务的代码
return 返回值;
}
- 访问修饰符:指定了方法的可见性,控制其他类是否可以访问该方法。常用的修饰符有public、private、protected和默认(没有修饰符)。
- 返回类型:指定了方法的返回值的类型。如果方法不返回任何值,可以使用 void 关键字。如果方法返回一个值,需要指定返回值的类型。
- 方法名:用于标识方法的名称。方法名必须是有效的标识符,并且应该具有描述性,以便于理解方法的功能。
- 参数列表:指定了方法的参数。参数是方法接受的输入值,可以有零个或多个参数。多个参数之间用逗号分隔。每个参数由参数类型和参数名组成。
- 方法体:包含了方法要执行的代码。方法体由花括号括起来,其中包含了一系列的语句,这些语句定义了方法的具体实现。
- 返回值:指定了方法的返回值。如果方法不返回任何值,可以省略该部分。如果方法返回一个值,需要使用 return 关键字返回一个与返回类型匹配的值。
需要注意的是,方法可以有不同的参数和返回类型,可以根据实际的需求来定义方法。在Java中,方法的使用可以使代码更加模块化和可重复使用,提高了代码的可读性和维护性。
各种方法
无参无返回值方法
实例:
public class Demo {
public static void main(String[] args) {
// 调用一下方法
getNum();
System.out.println("----------------------");
//再调用一下方法
getNum();
}
// 功能:可以从控制台接收2个数,并打印较大的。
public static void getNum(){
Scanner sc = new Scanner(System.in);
System.out.println("请输入第一个数:");
int n1 = sc.nextInt();
System.out.println("请输入第二个数:");
int n2 = sc.nextInt();
System.out.println("较大的是:" + (n1 > n2 ? n1 : n2));
}
}
有参无返回值的方法
形参:printHello(int a):其中的int a 就是"形参"- - 形式上的参数, 是在方法定义时设置的。
实参:printHello(10):是在"调用方法"时传入的"实际的参数"。
public static void main(String[] args) {
getMax(10, 20);// 10和20是实参
}
private static int getMax(int n, int m) {// n和m是形参
int max = n > m ? n :m;
System.out.println("最大值:" + max);
}
注意:“实参” 必须是 “形参” 的 “相同类型” 或 “兼容类型”, 而且 “实参” 的顺序必须要和 “形参” 的顺序一致!
有参有返回值的方法
public class Demo {
public static void main(String[] args) {
int a = 10;
int b = 20;
int c = 30;
int x = getMax(a, b);
int y = getMax(x, c);
System.out.println("最大值:" + y);
}
private static int getMax(int n, int m) {
int max = n > m ? n :m;
return max;
}
}
方法的注意事项
- 方法的定义位置:必须在"类中,方法外"。
方法中,可以调用方法,但不能定义方法。 - 方法必须"先定义,后调用"。不被调用,方法中的代码是不会执行的!
- 如果方法没有返回值,“返回值类型"要写void,表示"空”,什么都不返回,这样,此方法内部也不能return 值;
但可以写"return;"语句,表示:结束此方法的执行 - 如果方法声明处,声明了’非void’的"返回值类型",那么此方法内部任何一个代码分支都要保证return 值.
if (xxx) {
return xxx;
} else {
return xxx;
}
方法重载
重载:在一个类中,可以定义多个同名的方法,但“形参列表”不能完全相同,这种形式就叫:方法重载。
a).“形参列表”不能完全相同:指:类型、数量、顺序不能完全相同——跟**“变量名”**无关
b). 跟“返回值类型”没有关系!
同名不同参与返回值无关
好处:
- 定义方法时,多个功能相同的方法只起一个名字就可以;
- 调用方法时,就好像调用一个方法一样,也不用记那么多的名字。
this 关键字
this 关键字它指向当前对象的实例,它可以在类的方法中使用,用于引用当前对象的成员变量和成员方法。
作用
- 区分局部变量和实例变量:当局部变量和实例变量同名时,使用"this"关键字可以明确指示使用的是成员变量。例如:this.name = name;
- 在构造方法中调用其他重载的构造方法:在一个类中存在多个构造方法时,可以使用"this"关键字来调用其他构造方法,以避免代码重复。例如:this(name, age);
- 作为方法返回当前对象的引用:在方法中使用"return this;"语句可以返回当前对象的引用,以便进行链式调用。例如:public MyClass method() {return this;}
需要注意的是,"this"关键字只能在非静态方法或构造方法中使用,因为静态方法没有对具体对象的引用。同时,"this"关键字只能在类的内部使用,不能在静态方法中使用。
封装(Encapsulation)
私有成员变量,对外提供公共的get/set方法,提高对象属性的安全性。
封装的概念
将对象的 属性(数据)和 行为(方法)包装在一起,形成一个独立的单元。封装的目的是隐藏对象内部的实现细节,仅暴露必要的接口供外部访问。封装可以提高代码的可读性、可维护性以及安全性。在 Java 中,封装通常通过使用访问修饰符(如 public 、 private 和 protected )来实现。
private(私有)字段
private是一个关键字,用于“控制访问权限”。如果要控制“成员属性”不被其它类“直接访问”,就将这个成员属性修饰为:private(私有)私有后,此属性只能被本类的其他成员访问,不能被其它类访问。
get/set 方法调用
如下代码所示:在类的“成员方法”中,如果“局部变量”覆盖了“成员变量”,如果直接访问变量,访问的就是“局部变量”,就无法访问到被覆盖的“成员变量”。要想访问“成员变量”,需要使用“this.成员变量”的形式访问。
封装的代码实现
public class Person {
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private int age;
public String getName() {
return name;
}
public void setName(String n) {
【name = n;】
【this.name = n;】
}
public int getAge() {
return age;
}
public void setAge(int a) {
【age = a;】
【this.age = a;】
}
}
封装思想
- 封装概述:是面向对象三大特征之一(封装、继承、多态),是面向对象编程语言对客观世界的模拟,客观世界里成员变量都隐藏在对象内部,外界是无法操作的。
- 封装原则:将类的某些信息隐藏在类内部,不允许外界程序直接访问。通过该类提供的方法来实现对隐藏信息的操作和访问“成员变量private提供对应的getXxx()/setXxx方法”。
- 封装好处:通过方法来控制成员变量的操作,提高了代码的安全性,把代码用方法进行了封装,提高了代码的复用性。
继承(Inheritance)
继承的概念
子类继承父类,子类就会自动拥有父类所有的成员变量和成员方法。子类可以重写(覆盖)或扩展父类的方法。继承的主要目的是实现代码的复用,减少重复代码,提高程序的可维护性。在 Java 中,继承使用 extends 关键字表示。
在实际使用中可以把多个类中的共性,抽取出来,放在一个父类中。
继承的代码实现
public class Person {
/**
* 姓名
*/
String name;
/**
* 年龄
*/
int age;
/**
* 吃饭功能
*/
public void eat() {
System.out.println(name + "吃饭");
}
/**
* 睡觉功能
*/
public void sleep() {
System.out.println(name + "睡觉");
}
}
public class Teacher extends Person {
/**
* 薪资
*/
int salary;
/**
* 教学功能
*/
public void teach() {
System.out.println(name + "讲课");
}
}
public static void main(String[] args) {
// 创建教师对象
Teacher teacher = new Teacher();
// 使用Teacher类继承自Person父类的属性
teacher.name = "王老师";
teacher.age = 35;
// 使用Teacher本身的属性
teacher.salary = 8000;
// 使用Teacher类继承自Person父类的方法
teacher.eat();
teacher.sleep();
// 使用Teacher本身的方法
teacher.teach();
}
王老师吃饭
王老师睡觉
王老师讲课
继承特点
- Java只支持单继承,不支持多继承。(因为在调用父类属性的时候不能确定是哪个父类的属性)
- Java支持多重继承。
- Java支持一个类可以有多个子类。
继承的注意事项
- 父类的构造方法是子类不能继承的,构造方法是创建给本类对象是用的。父类使用父类的构造方法,子类使用子类的构造方法。
- 父类的私有成员,子类是不能直接使用的,私有的只有本类可以使用,子类可以通过get/set方法使用。
继承后的成员变量和成员方法
- 子类中有使用子类的。
- 子类中没有使用继承父类的。
- 子类父类中都没有,编译报错。
多态(Polymorphism)
多态的概念
父类的变量指向子类的对象,接口的变量指向实现类对象。
多态是指同一个方法名在不同对象(类)中具有不同的实现。多态允许我们将不同类的对象视为同一类型,通过这种方式,我们可以编写出更加灵活和可扩展的代码。在 Java 中,多态主要通过接口、抽象类和方法重写来实现。多态的实现依赖于 Java 的动态方法分派机制。
格式
父类变量 变量名 = new 子类对象();
父类接口 变量名 = new 实现类对象();
多态的代码实现
定义父类
public class Person {
public void play() {
System.out.println("Person的work方法");
}
}
定义子类
public class Teacher extends Person {
public void play() {
System.out.println("Teacher重写Person的play方法");
}
}
public class Student extends Person{
public void play() {
System.out.println("Student重写Person的play方法");
}
}
测试
public static void main(String[] args) {
// 多态
Person p1 = new Teacher();
Person p2 = new Student();
// 子类重写后的方法
p1.play();
p2.play();
}
Teacher重写Person的play方法
Student重写Person的play方法
向上转型和向下转型
-
向上转型:多态的本身就是向上转型,把子类的对象赋值给父类。
优点:扩展性强,赋值不同的子类对象,调用不同子类重写后的方法。
缺点:无法使用子类特有的功能。
格式:父类类型 变量名 = new 子类对象(); int a = 10; —> double d = a;
-
向下转型:多态的父类变量,强制转换为子类类型。
优点:父类类型转换为子类类型,可以使用子类特有的功能。
缺点:扩展性差。
格式:子类类型 变量名 = (子类类型)父类变量名。 double d = 10; —> int a = (int)d;
前提:向下转型的前提必须是多态,直接创建父类对象不能转型。
instanceof关键字
作用:判断某个对象是否属于某种数据类型,可以在向下转型之前先判断。
格式:boolean b = 对象 instanceof 数据类型; return:true/false
注意:对象是根据类创建对象,对象所属的类要判断的数据类型之间必须有继承或者实现关系才能判断。
public static void main(String[] args) {
Person p = new Teacher();
if (p instanceof Teacher) {
Teacher t = (Teacher) p;
t.play();
}
}
多态的特点
- 多态调用的是子类重写后的方法,如果没有方法重写,多态则没有意义。
- 多态指向哪个对象,就调用哪个子类对象重写后的方法。
优点:扩展性强,父类的引用变量可以赋值不同的子类对象,而调用不同子类重写后的方法。
缺点:无法使用子类也有的成员。
工作中一般使用父类作为方法的参数或返回值,调用方法可以传递子类对象或返回子类对象。
抽象(Abstraction)
有一个比较特殊的特性“抽象”,有部分人认为Java面向对象的特征有四个,抽象也是特征之一。
但我个人不那么认为,因为抽象是软件开发中一个普遍的特性,准确的说“抽象”不能归类到 Java 面向对象的特征中。
抽象的概念
抽象是指将具有相似特征的对象抽取出共同的属性和行为,形成一个更抽象的概念。在 Java 中,抽象可以通过抽象类 abstract 类 和 接口 interface 来实现。抽象类和接口可以定义一组规范,具体的实现可以交给继承抽象类或实现接口的子类。抽象有助于降低类之间的耦合度,提高程序的扩展性。
抽象的格式
权限修饰符 abstract 返回值类型 方法名(参数列表);
-
抽象类无法直接创建对象使用。
抽象类一般都包含抽象方法,抽象方法没有方法体,创建对象调用方法没有意义。
有些类就是不想让别人创建对象使用,也可以定义为抽象类(抽象类可以不包含抽象方法)。
-
需要定义子类,继承抽象父类,重写父类的抽象方法,创建子类对象使用。
抽象类代码实现
父类:抽取子类共同的成员变量和成员方法而组成。
public abstract class Person {
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private int age;
/**
* 抽象出工作方法
*/
public abstract void work();
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
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;
}
}
/**
* 子类继承抽象父类后,有两种方式:
* 1、把子类修改为抽象类。
* 2、重写父类的抽象方法,添加方法体。
*/
public class Teacher extends Person {
@Override
public void work() {
System.out.println(getName() + "正在从事教育工作");
}
}
public class Programmer extends Person {
/**
* 调用父类的空参构造方法
*/
public Programmer() {
super();
}
/**
* 调用父类的满参构造方法
*/
public Programmer(String name, int age) {
super(name, age);
}
@Override
public void work() {
System.out.println(getName() + "正在搬砖");
}
}
public abstract class Scientist extends Person {
}
public static void main(String[] args) {
/**
* 创建子类Teacher对象
*/
Teacher t = new Teacher();
t.setName("王老师");
t.setAge(35);
t.work();
/**
* 创建子类Programmer对象
*/
Programmer p = new Programmer("老王", 18);
p.work();
// Scientist是一个抽象类,不能直接创建对象使用
// Scientist scientist = new Scientist();
}
抽象类的特点
抽象类的特点总结起来可以说是有得有失。
得:抽象类得到了抽象方法的能力。
失:抽象类失去了创建对象的能力。
其他成员(构造器(构造方法),成员方法,静态方法等)抽象类都是具备的。
注意事项
- 抽象类不能创建对象,如果创建会因为编译无法通过而报错,假设创建了抽象类的对象调用抽象方法,而抽象方法没有具体的方法体没有意义。但可以创建其非抽象子类的对象。
- 抽象类中,可以有构造方法,是供子类创建对象时初始化父类成员使用的。子类的构造方法中有默认的super(),需要访问父类构造方法。
- 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通过用于某些特殊的类结构设计。
- 抽象类的子类,必须重写抽象父类中所有的抽象对象,否则,编译无法通过而报错。假设不重写所有抽象方法,则类中可能包含抽象方法,那么创建对象后调用抽象方法没有意义。除非该子类也是抽象类。
抽象类存在的意义
抽象类存在的意义就是为了被子类继承,否则抽象类将毫无意义。
抽象方法就是为了让子类重写,例如规定公司中所有人必须工作,必须重写工作的方法。
接口(Interface)
概述
Java中的接口是一种抽象的引用数据类型,是功能的集合。接口可以包含常量和抽象方法的声明,但不能包含实例变量或具体方法的实现。
接口实现
接口中没有构造方法,也不能创建对象使用。
public interface MyInterface {
// 接口中的抽象方法,修饰符默认为 public abstract,可以省略不写
void myMethod01();
// 接口中默认方法的 default 关键字不能省略
default int myMethod02(int a, int b) {
return a + b;
}
// 接口中的静态方法 static 关键字不能省略,可以通过接口名直接使用
static String myMethod03() {
return "static";
}
}
public class MyInterfaceImpl implements MyInterface {
@Override
public void myMethod01() {
System.out.println("Doing myMethod01...");
}
@Override
public int myMethod02(int a, int b) {
return a + b;
}
}
注意:
- 接口中的静态方法不能重写,也不能继承使用。
- 类中的静态方法不能重写,可以继承使用。
接口多实现
一个类可以实现一个或多个接口。
public interface A {
void a();
}
public interface B {
void b();
}
public class ABImpl implements A,B {
@Override
public void a() {
System.out.println("Doing A a");
}
@Override
public void b() {
System.out.println("Doing B b");
}
}
接口和抽象类的区别
接口和抽象类在Java中都用于实现类的继承和多态性,但它们之间有一些重要的区别:
- 定义:接口使用 interface 关键字定义,抽象类使用 abstract 关键字修饰类。
- 继承:一个类可以同时继承一个抽象类并实现多个接口。抽象类可以提供一些通用的方法实现,而接口只能声明方法,不提供实现。抽象类可以拥有实例变量,而接口只能拥有常量。
- 实现:实现接口的类必须提供接口中所有抽象方法的具体实现,而继承抽象类的子类可以选择性地覆盖抽象方法。一个类可以实现多个接口,但只能继承一个抽象类。
- 构造函数:抽象类可以有构造函数,而接口不能有构造函数。这是因为接口只是一种行为规范,不包含实例变量,所以不需要构造函数。
- 可见性:接口中的方法默认为 public ,而抽象类可以定义不同的可见性( public 、 protected 、 private )。接口中的方法默认为 public ,不需要显式声明。抽象类可以包含非抽象方法,它们可以有不同的可见性。
- 用途:接口主要用于定义类的行为规范,实现类需要提供具体的实现。抽象类主要用于作为其他类的基类,提供一些通用的方法实现,可以包含一些具体方法。
总的来说,接口更加抽象和规范,只定义方法的声明,没有任何实现。抽象类则更接近于普通类,可以包含实例变量和具体方法的实现。在设计时,需要根据具体情况来选择使用接口还是抽象类。如果需要定义类的行为规范,并且这些类之间没有共享的实例变量,那么应该使用接口。如果需要定义共享的实例变量或提供一些通用的方法实现,那么应该使用抽象类。
构造方法
创建对象就是调用类中的构造方法。
构造方法的定义格式
- 构造方法没有返回值类型也没有返回值。
- 构造方法的名字必须和类名一模一样。
构造方法的特点
- 类中如果没有创建空参构造方法,java 会默认提供一个空参数构造方法,public Person() {}。
- 类中如果创建了空参构造方法,java 就不会再提供默认的空参构造方法了,要使用空参构造方法,必须手动写出。
- 构造方法可以重载。如:空参构造、满参构造。
final 关键字
final 修饰的类
表示是一个最终类,该类是不可被继承的,即不能有子类。这是为了防止其他类对该类进行修改或继承,以确保类的完整性和稳定性。
final 修饰的方法
表示是一个最终方法,该方法是不可被子类重写的。这通常用于确保方法的逻辑不被修改,或者是为了提高执行效率,因为编译器可以对final方法进行优化。
final 修饰的变量
表示该变量是一个常量,即它的值在初始化后不能被修改。常量一般使用大写字母命名,并在声明时进行初始化。
final变量可以在声明时直接初始化,或者在构造函数或初始化块中初始化。
使用final关键字的好处
- 提高安全性:final关键字可以防止类、方法和变量被修改,确保其不被意外改变。
- 提高性能:final关键字可以让编译器进行优化,例如对final方法进行内联优化。
- 提高可读性:final关键字可以明确表示该部分不可被修改,使代码更易于理解。
需要注意的是,final关键字的使用应根据具体情况谨慎选择,过度使用可能会导致代码的可扩展性和灵活性降低。
static 关键字
静态内部类
当一个类中定义的另一个被 static 修饰的类,该类就叫做静态内部类。静态内部类可以直接访问外部类的静态成员,但不能直接访问外部的非静态成员。
静态代码块
它可以用来初始化静态成员变量或执行一些静态的初始化操作。静态代码块在 Java 虚拟机加载类时按照顺序只执行一次,并且在类的所有实例化对象之前执行。
静态方法
当一个方法被 static 修饰,该类就叫做静态方法。静态方法不依赖于对象的实例,可以直接通过类名调用。静态方法不能访问非静态成员,因为它没有隐式的 this 参数。
使用:工具类静态方法。
静态变量
当一个变量被 static 修饰,该变量就叫做静态变量,也叫作类变量。静态变量属于类,不属于对象。静态变量在类加载时被初始化,只有一份副本存在内存中,所有对象共享同一份静态变量。
使用:常量类定义常量。
对象内存存储
在Java中,对象在内存中存储分为两个部分:堆(heap)和栈(stack)。
- 堆(heap):所有的对象都存储在堆中。堆是一个动态分配的内存区域,用于存储类的实例对象。当使用new关键字创建一个对象时,它会在堆中分配一块内存空间来存储对象的数据和实例变量。堆中的对象可以被多个引用变量引用,当没有任何引用指向该对象时,垃圾回收机制会自动回收该对象的内存。
- 栈(stack):栈用于存储方法调用和局部变量。每个线程在执行方法时,都会在栈中创建一个栈帧(stack frame),用于存储方法的参数、局部变量和方法返回值等信息。栈的特点是先进后出,每个栈帧在方法执行完毕后会被弹出栈空间,从而释放其占用的内存。
对象的引用(变量)存储在栈中,而对象本身存储在堆中。当创建一个对象时,栈中的引用变量会指向堆中对象的地址。通过引用变量,我们可以访问和操作堆中的对象。
需要注意的是,Java中的基本类型数据(如int、float等)直接存储在栈中,而不是在堆中。只有对象才会存储在堆中。
匿名对象
在创建对象的时候,只有创建语句没有把对象赋值给某一个变量,叫匿名对象。没有名字的对象。
特点:匿名对象只使用一次,使用完毕会被JVM在空闲的时候垃圾回收,可以节约内存。
作用:当在对象只是用一次的时候,就可以使用匿名对象。在工作中匿名对象一般用来作为参数和返回值或直接往集合中添加匿名对象。
内部类
指在一个类的内部定义的另一个类。在Java中,内部类可以分为四种类型:成员内部类、局部内部类、匿名内部类和静态内部类。
成员内部类
写在类中方法外的叫做成员内部类,可以访问外部类的所有成员,包括私有成员。
使用方式:外部类名.内部类名 变量名 = new 外部类().new 内部类()。
使用成员内部类可以实现更好的封装性和组织性。
静态内部类
定义在外部类的成员位置,并且被static修饰,可以直接通过外部类访问,不需要实例化外部类。
使用方式为:外部类名.内部类名 内部类对象名 = new 外部类名.内部类构造器。
局部内部类
写在方法或代码块内部的叫做局部内部类,作用范围仅限于方法或代码块内部。
局部内部类可以访问外部类的成员,但是外部类不能访问局部内部类的成员。
匿名内部类
概述
匿名内部类是一种没有名字的内部类,它用于创建一个实现某个接口或继承个类的对象。它的定义和实例化同时进行,通常用于在程序中定义一次性的、简单的类。
使用场景
实现接口
public class Main {
public static void main(String[] args) {
new Fly(){
@Override
public void fly() {
System.out.println("没有名字的鸟在飞!");
}
}.fly();
}
}
public interface Fly {
public abstract void fly();
}
继承父类
public class Main {
public static void main(String[] args) {
new Animal(){
@Override
public void eat() {
System.out.println("动物在吃饭!");
}
@Override
public void sleep() {
System.out.println("动物在睡觉!");
}
}.eat();
}
}
public abstract class Animal {
public abstract void eat();
public abstract void sleep();
}
匿名内部类的特点
- 没有明确的类名,直接定义类的实现。
- 只能实现一个接口或继承一个类,不能同时实现多个接口或继承多个类。
- 可以在创建对象的同时定义类的实现。
- 通常用于创建临时的、只需在特定场景下使用的类对象。
注意事项
- 不能定义构造方法,因为没有明确的类名。
- 不能定义静态成员,因为没有类名来限定。
- 只能在创建对象的地方使用,无法在其他地方引用。
总体来说,匿名内部类是一种简洁方便的方式来实现接口或继承类的临时对象。它在某些场景下可以提供更好的代码可读性和组织性。
代码块
被{ }包裹起来的一段代码。
构造代码块
构造代码块在类中定义,没有访问修饰符和方法名,会在每次创建对象时执行。构造代码块主要用于初始化对象的共同操作,可以减少代码重复。
public class Example {
{
// 构造代码块
System.out.println("构造代码块");
}
public Example() {
System.out.println("构造方法");
}
}
静态代码块
静态代码块在类中定义,使用 static 修饰符,会在类加载时执行,且只会执行一次。静态代码块主要用于初始化静态变量和执行静态方法。
public class Example {
static {
// 静态代码块
System.out.println("静态代码块");
}
public static void main(String[] args) {
System.out.println("主方法");
}
}
普通代码块
普通代码块也称为局部代码块,定义在方法中或类的成员方法中。它没有访问修饰符,只能在所在的方法中使用。普通代码块主要用于控制变量的作用域和执行特定的功能。
public class Example {
public void method() {
// 普通代码块
{
int x = 10;
System.out.println("x = " + x);
}
// x 在普通代码块外不可见
}
}
注解
概念
Java 注解是一种元数据的形式,可以添加到类、方法、字段或其他程序元素上,以提供额外的信息和配置。注解以@
符号开头,后跟注解的名称和可选的参数列表。
- 元数据:注解是元数据的一种形式,提供了关于程序元素的附加信息。它们可以用于描述和补充代码中的各个部分,使得代码更易读和理解。
- 可读性和维护性:注解可以提高代码的可读性和可维护性,通过向代码中添加附加信息,使其更具表达力和意义。
- 扩展性:注解可以扩展代码的功能和行为。通过使用注解,可以实现各种功能,如配置、代码生成、运行时处理等。
作用
- 配置和元数据:通过注解,可以在代码中配置和指定某些元数据。这些元数据可以被用于各种用途,如配置文件、框架、库等。
- 编译时处理:注解可以用于编译时的处理。通过定义注解处理器,可以在编译时对注解进行解析和处理,生成额外的代码或执行特定的操作,以实现自动化或定制化的任务。
- 运行时处理:注解可以在运行时使用。通过使用反射机制,可以在运行时获取注解的信息,并根据注解的配置执行相应的逻辑,实现动态配置和行为。
- 代码检查和验证:注解可以用于进行代码检查和验证。定义自定义的注解,并在编译或运行时进行检查,可以帮助发现潜在的问题、错误或违规的代码。
- 框架和库的扩展:注解被广泛用于实现框架和库,以提供更多的扩展点和自定义化的功能。通过使用注解,开发者可以与框架或库进行交互,并在指定的位置添加自己的逻辑。
Java 注解提供了一种注释和配置程序元素的机制。它们可以用于提供元数据、进行编译时处理、执行运行时逻辑、进行代码检查和验证,以及实现框架和库的扩展。通过使用注解可以提高代码的可读性、可维护性,并实现各种功能和特性。
内置注解
Java 提供了一些内置的注解,用于特定的目的和场景。以下是一些常用的内置注解:
@Override
:用于标记方法重写父类或接口中的方法。当开发人员意图重写一个方法时,使用此注解可以确保方法的正确性。如果使用了该注解但没有重写对应的方法,编译器会生成错误。@Deprecated
:用于标记已过时的元素(类、方法、字段等)。标记为过时的元素意味着不推荐使用,可能会在将来的版本中被移除或替代。开发人员应该尽快停止使用这样的元素并寻找替代方案。@SuppressWarnings
:用于抑制编译器警告。在某些情况下,编译器会生成警告,但是开发人员知道代码是正确的。通过使用@SuppressWarnings
注解,可以告诉编译器忽略特定类型的警告。@SafeVarargs
:用于标记可变参数方法(Varargs)的安全性。当开发人员确信使用可变参数方法不会引发类型不安全警告时,可以使用此注解来消除警告。@FunctionalInterface
:用于标记函数式接口。函数式接口是只包含一个抽象方法的接口,该注解可以确保接口只有一个抽象方法。这对于使用 Lambda 表达式和函数式编程很有用。@SuppressWarnings("unchecked")
:用于抑制无关的警告。由于类型擦除机制,某些情况下编译器无法进行类型检查。使用此注解可以告诉编译器忽略类型检查相关的警告。
内置注解提供了一种简单而有效的方式,用于在编码中传达额外的元数据和信息,提高代码的可读性和可维护性。使用这些注解可以帮助开发人员正确地编写代码,以及消除潜在的警告和错误。除了上述的几个内置注解外,Java 还提供了其他一些内置注解,如@Retention
、@Target
、@Documented
等,用于控制注解的行为和用法。每个内置注解都有特定的语义和作用,可以在代码中使用以增强其功能和可读性。
元注解
Java 的元注解是用于定义注解的注解,也就是注解的元数据。通过使用元注解,我们可以对注解的行为和用法进行更加精确和细致的控制。Java 提供了一些常用的元注解,下面对它们进行详细介绍:
@Retention
:@Retention
用于指定注解的保留策略,即注解在什么级别可用。它可以应用于注解类型本身。常见的保留策略包括:RetentionPolicy.SOURCE
:注解只在源代码中可见,编译器将丢弃它们。RetentionPolicy.CLASS
:注解在编译时保留,在运行时被丢弃。这是默认的保留策略。RetentionPolicy.RUNTIME
:注解在编译和运行时都保留,并可以通过反射在运行时访问。
@Target
:@Target
用于指定注解可以应用于哪些程序元素。它可以应用于注解类型本身。常见的元素类型包括:ElementType.TYPE
:类、接口、枚举ElementType.METHOD
:方法ElementType.FIELD
:字段ElementType.PARAMETER
:方法参数- 等等
@Documented
:@Documented
用于指定注解是否会出现在生成的文档中。如果一个注解被标记为@Documented
,那么它会包含在生成的文档中。@Inherited
:@Inherited
用于指定注解是否可以被子类继承。如果一个注解被标记为@Inherited
,那么它会被子类继承。
通过结合使用这些元注解,我们可以更加精确地定义和控制我们自定义的注解的行为和用法。它们为我们提供了更高的灵活性和可扩展性,以使我们的注解更好地符合我们的需求和预期。除了上述的元注解之外,还有其他一些元注解可供使用,如@Repeatable
、@Native
、@FunctionalInterface
等等,每个元注解都有自己特定的用途和适用范围。
这些元注解提供了更精确地控制和定义注解的行为和用法,可以根据需要灵活地设计和使用自定义注解。在定义自己的注解时,可以使用这些元注解来指定注解的保留策略、适用的元素类型、是否包含在文档中以及是否可以继承。
注意:元注解本身也可以被其他注解标注,以实现更复杂的注解行为。使用元注解和自定义注解可以为代码添加丰富的元数据,以便在编程和运行时进行处理和利用。
自定义注解
在 Java 中,自定义注解允许开发者创建自己的元数据,以在代码中添加额外的信息和配置。自定义注解由 @interface
关键字进行定义,类似于接口的声明。以下是关于 Java 自定义注解的详细说明:
-
定义自定义注解:
// 定义注解类型 public @interface MyAnnotation { // 定义成员变量,可以指定默认值 String value() default ""; int count() default 0; }
可以通过
@interface
关键字来定义自定义注解。注解中可以定义成员变量,可以在需要的时候为它们指定默认值。注解的成员变量类型可以是基本类型、枚举、注解类型、数组以及这些类型的包装类。 -
使用自定义注解:
// 使用自定义注解 @MyAnnotation(value = "example", count = 10) public class MyClass { // 类定义... }
在代码中使用自定义注解时,可以通过注解名称加括号的方式为注解的成员变量赋值。也可以选择使用默认值。
-
保留策略:
注解默认的保留策略是RetentionPolicy.CLASS
,即在编译时被保留,但在运行时被丢弃。如果需要在运行时访问注解信息,可以通过在自定义注解上添加@Retention(RetentionPolicy.RUNTIME)
声明。 -
目标元素:
自定义注解可以应用于不同的程序元素,如类、方法、字段、参数等。可以通过在自定义注解上添加@Target(ElementType.XXX)
声明来指定它可以应用的具体元素类型。例如,@Target(ElementType.TYPE)
指定注解可以应用于类。 -
元注解的使用:
元注解是用于控制和配置注解行为的注解。例如,通过使用@Retention
和@Target
元注解,可以控制注解的保留策略和适用目标。还有其他一些元注解可用,如@Documented
、@Inherited
等。
自定义注解提供了一种扩展 Java 语言的机制,可以为代码添加额外的元数据,以实现各种功能和需求。通过元数据的方式,在编译时、运行时或通过自定义的注解处理器进行处理。我们可以使用反射来访问和解析注解,从而实现动态配置和行为。自定义注解在配置、代码生成、框架和库扩展等方面有着广泛的应用。
注解和反射的综合案例
首先,我们定义一个自定义注解@Serializable
,用于标记需要进行序列化和反序列化的字段:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Serializable {
}
接下来,我们创建一个包含@Serializable
注解的类Person
,并实现自定义的序列化和反序列化逻辑:
import java.lang.reflect.Field;
public class Person {
@Serializable
private String name;
@Serializable
private int age;
public static void serialize(Object obj) throws IllegalAccessException {
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Serializable.class)) {
field.setAccessible(true);
System.out.println("Serializing field: " + field.getName() + ", value: " + field.get(obj));
// 在这里可以将字段值转换为字节流或其他序列化格式
}
}
}
public static void deserialize(Object obj) throws IllegalAccessException {
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Serializable.class)) {
field.setAccessible(true);
// 在这里可以从字节流或其他序列化格式中恢复字段值
// 这里只是简单地输出反序列化的结果
System.out.println("Deserialized field: " + field.getName() + ", value: " + field.get(obj));
}
}
}
public static void main(String[] args) throws IllegalAccessException {
Person person = new Person();
person.name = "John";
person.age = 30;
serialize(person);
deserialize(person);
}
}
在上述示例中,我们使用了注解@Serializable
来标记需要进行序列化和反序列化的字段。然后,通过反射和注解的结合,我们可以在serialize
和deserialize
方法中遍历类的字段,找到标记了@Serializable
注解的字段,并执行相应的序列化和反序列化操作。
请注意,这个示例只是一个简单的演示,实际中的序列化和反序列化过程可能更加复杂,需要考虑字节流的读写、数据格式的转换等。然而,通过注解和反射的结合,我们可以实现更加灵活和可扩展的序列化和反序列化逻辑,同时保持代码的可读性和维护性。
注解处理器
Java 注解处理器是用于解析和处理注解的工具,它可以在编译时或运行时对注解进行处理。注解处理器通过扫描和分析源代码中的注解,然后根据注解的配置执行相应的逻辑。以下是关于 Java 注解处理器的详细说明:
- 注解处理器接口:
Java 提供了javax.annotation.processing.Processor
接口,用于定义注解处理器的基本行为。自定义的注解处理器需要实现这个接口,并重写其中的方法。 - 注解处理器的工作流程:
注解处理器通常在编译时执行,通过 javac 编译器调用。在编译过程中,注解处理器会被触发并执行,处理指定的注解。注解处理器可以读取、修改和生成源代码,也可以生成额外的文件。 - 定义注解处理器:
自定义的注解处理器需要实现javax.annotation.processing.Processor
接口,并重写其中的方法。这些方法包括getSupportedAnnotationTypes
、getSupportedSourceVersion
、process
等,用于指定支持的注解类型、源代码版本和处理逻辑。 - 注册注解处理器:
在编译时,注解处理器需要通过配置文件或命令行参数进行注册。可以在META-INF/services/javax.annotation.processing.Processor
文件中列出注解处理器的全限定名,或者使用-processor
选项指定注解处理器。 - 使用注解处理器:
使用注解处理器的最常见方法是通过构建工具(如 Maven 或 Gradle)来集成和配置。构建工具可以自动加载和执行注解处理器,而无需手动注册。 - 第三方注解处理器:
除了自定义的注解处理器,还有一些第三方的注解处理器可供使用。这些注解处理器能够处理常见的用例,如生成代码、检查和验证等。
通过使用注解处理器,开发者可以扩展注解的功能和应用场景,实现自动化的任务和代码生成。注解处理器可以在编译时检查代码的正确性、生成额外的代码、实现框架和库的扩展性等。注解处理器在开发各种工具、框架和库时,发挥着重要的作用。
使用
Java 注解是一种元数据形式,用于向程序元素添加额外的信息和配置。下面是关于 Java 注解的使用细节:
-
应用于类、接口、枚举:
@AnnotationName public class MyClass { // 类定义... }
-
应用于方法:
public class MyClass { @AnnotationName public void myMethod() { // 方法定义... } }
-
应用于字段:
public class MyClass { @AnnotationName private int myField; }
-
应用于方法参数:
public class MyClass { public void myMethod(@AnnotationName int param) { // 方法定义... } }
-
使用带有参数的注解:
@AnnotationName(parameter1 = value1, parameter2 = value2) public void myMethod() { // 方法定义... }
-
多个注解的应用:
@Annotation1 @Annotation2 public class MyClass { // 类定义... }
-
获取注解信息:
Annotation annotation = clazz.getAnnotation(AnnotationName.class); if (annotation != null) { // 处理注解逻辑... }
在使用注解时,可以根据需要选择是否使用注解的参数,并为其指定值。注解可以应用于不同的程序元素,包括类、方法、字段、方法参数等。多个注解可以同时应用于同一个程序元素,为其提供多个不同的注解信息。
可以使用反射机制获取注解信息,例如使用 getAnnotation(AnnotationName.class)
方法获取指定注解类型的注解,并进一步处理注解的信息。
同时,Java 还提供了一些内置的注解,如@Override
、@SuppressWarnings
、@Deprecated
等。这些内置注解可以应用于类、方法等,提供了特定的语义和行为。
通过合理使用注解,可以提高代码的可读性和可维护性,同时可以实现各种功能和特性,如配置、代码生成、运行时处理等。
枚举
概念
枚举是一种在编程中表示固定常量集合的特殊类型。在 Java 中,枚举是一种引用类型,用于声明带有一组具名值的特定类型。
- 枚举类型 (Enum Type) 是一种特殊的类。
- 枚举常量 (Enum Constants) 是枚举类型的实例,它们是程序中固定的、有限且唯一的。
- 枚举常量可以具有属性、方法和构造函数。
枚举是一种在 Java 中常用的数据类型,被广泛应用于不同的场景,如表示固定的常量集合、状态机、选项、错误码等。通过使用枚举,可以使代码更加清晰、可读和可维护,并减少程序错误的可能性。
作用
- 常量集合:枚举提供了一种简洁、类型安全的方式来表示一组固定的常量值。
- 可读性和可维护性:使用枚举可以使代码更具可读性和可维护性。枚举常量具有明确的含义,增强了代码的可读性。
- 避免魔法值:枚举常量可以用于避免在代码中使用魔法值(Magic Value)。通过使用枚举常量来表示特定的常量值,可以使得代码更加清晰和可维护。
- 安全性:枚举提供了类型安全的常量集合,可以减少错误和bug的发生。
- 约束和限制值的范围:枚举常量可以约束某个变量的有效取值范围,减少错误和非法值的产生。
语法
- 定义枚举类型:使用
enum
关键字来定义枚举类型,语法如下:
enum EnumName {
// 枚举常量列表
}
其中,EnumName
是枚举类型的名称,可以根据需求自定义。
- 枚举常量列表:在枚举类型内部,列出枚举常量,语法如下:
enum EnumName {
CONSTANT1,
CONSTANT2,
// ...
CONSTANTn
}
每个枚举常量用逗号分隔,通常大写字母形式命名。枚举常量的数量是固定的,且每个常量都是唯一的。
- 枚举常量的属性和方法:枚举常量可以具有属性和方法,就像普通的类一样。示例如下:
enum EnumName {
CONSTANT1("Value1"),
CONSTANT2("Value2");
private String value;
EnumName(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
在上述示例中,每个枚举常量都有一个关联的值,并且定义了一个构造函数和一个方法。
- 获取枚举常量:可以通过枚举常量的名称来获取相应的枚举对象。示例如下:
EnumName enumValue = EnumName.CONSTANT1;
- switch 语句中使用枚举:可以在 switch 语句中使用枚举常量作为分支选择条件。示例如下:
EnumName enumValue = EnumName.CONSTANT1;
switch(enumValue) {
case CONSTANT1:
// 逻辑处理
break;
case CONSTANT2:
// 逻辑处理
break;
// ...
default:
// 默认逻辑处理
break;
}
- 遍历枚举常量:可以使用
EnumName.values()
方法来遍历枚举常量数组。示例如下:
for (EnumName enumValue : EnumName.values()) {
// 逻辑处理
}
在遍历时,会依次迭代枚举常量数组中的每个枚举常量。
Java 枚举的语法相对简洁且易于理解。通过使用枚举常量、属性和方法,可以实现更加清晰、类型安全和可读性强的代码。枚举常量的限定数量保证了代码的安全性,并且枚举类型的语法规则为我们提供了一些方便的功能,如遍历、switch 分支等。
特性
Java中的枚举是一种特殊的数据类型,用于定义一组常量。它具有以下特性:
- 有限的实例:枚举限制为预定义的一组常量实例。这意味着枚举类型的变量只能取枚举中定义的值,而不能取其他任意值。
- 常量命名:枚举实例通常用大写字母表示,并且在枚举中的每个实例之间使用逗号分隔。
- 类型安全:枚举提供了类型安全,编译器可以在编译时检查枚举值的类型。
- 单例实例:每个枚举值都是枚举类型的单例实例,可以通过枚举值访问其实例方法和字段。
- 迭代支持:枚举类型可以使用foreach循环进行迭代,便于遍历枚举中的所有实例。
- 方法和字段:枚举可以包含方法和字段,因此可以为每个枚举值定义不同的行为。
- 可以实现接口:枚举可以实现接口,这使得枚举类型可以具有方法和行为。
通过使用枚举,可以更清晰地表示一组相关的常量,并且可以减少错误和增加代码的可读性。枚举在处理有限的选项集或预定义的常量时非常有用,例如表示星期几、月份、颜色等。
常见用法
- 代表有限的选项集:枚举非常适合用于表示有限的选项集。例如,可以使用枚举来表示一周的每一天,月份或颜色。
enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
- 替代常量:枚举可以用作替代传统的常量定义。通过使用枚举,可以将相关的常量分组在一起,并提供更好的可读性和维护性。
enum Status {
SUCCESS, FAILURE, PENDING
}
- 状态机:枚举可以用于表示状态机。通过定义不同的枚举实例来表示不同的状态,并使用方法和逻辑来进行状态转换。
enum State {
START, PROCESSING, COMPLETED, ERROR
}
- 单例模式:枚举可以用于实现单例模式。枚举中的每个枚举值都是单例实例,保证了线程安全和序列化的一致性。
enum Singleton {
INSTANCE;
public void doSomething() {
// 单例实例的方法逻辑
}
}
- 实现接口:枚举可以实现一个或多个接口,从而为枚举类型添加方法和行为。
enum Operation {
ADD {
public int apply(int a, int b) {
return a + b;
}
},
SUBTRACT {
public int apply(int a, int b) {
return a - b;
}
};
public abstract int apply(int a, int b);
}
这些只是Java枚举的一些常见用法,它们提供了一种有用的方式来组织和表示常量集合,并增强了代码的可读性和可维护性。
高级特性
- 枚举构造函数和实例字段:枚举可以具有构造函数和实例字段,可以为每个枚举值定义不同的属性和行为。
enum Color {
RED(255, 0, 0),
GREEN(0, 255, 0),
BLUE(0, 0, 255);
private int red;
private int green;
private int blue;
Color(int red, int green, int blue) {
this.red = red;
this.green = green;
this.blue = blue;
}
public int getRed() {
return red;
}
public int getGreen() {
return green;
}
public int getBlue() {
return blue;
}
}
- 枚举的静态方法:枚举可以定义静态方法,这些方法可以在枚举类型上进行调用,而不仅仅是在枚举值上调用。
enum MathOperation {
ADD {
public int apply(int a, int b) {
return a + b;
}
},
SUBTRACT {
public int apply(int a, int b) {
return a - b;
}
};
public abstract int apply(int a, int b);
public static int calculate(MathOperation operation, int a, int b) {
return operation.apply(a, b);
}
}
使用示例:
int result = MathOperation.calculate(MathOperation.ADD, 5, 3);
System.out.println(result); // 输出:8
- 使用接口进行扩展:枚举可以实现接口,并且可以使用接口的默认方法来为每个枚举值提供统一的行为。
interface Operation {
int apply(int a, int b);
default void printResult(int a, int b) {
int result = apply(a, b);
System.out.println("Result: " + result);
}
}
enum MathOperation implements Operation {
ADD {
public int apply(int a, int b) {
return a + b;
}
},
SUBTRACT {
public int apply(int a, int b) {
return a - b;
}
};
}
使用示例:
MathOperation.ADD.printResult(5, 3); // 输出:Result: 8
这些高级特性使得Java枚举更加灵活和功能强大,可以根据需要定义枚举的属性、行为和接口实现。
项目中的枚举使用案例
- 表示状态或类型:枚举可用于表示状态或类型的固定集合。例如,一个订单可以具有不同的状态,如"待处理"、“已发货"和"已完成”。
enum OrderStatus {
PENDING,
SHIPPED,
COMPLETED
}
class Order {
private OrderStatus status;
public OrderStatus getStatus() {
return status;
}
public void setStatus(OrderStatus status) {
this.status = status;
}
}
Order order = new Order();
order.setStatus(OrderStatus.PENDING);
if (order.getStatus() == OrderStatus.PENDING) {
// 执行待处理订单的逻辑
}
- 定义选项列表:枚举可用于定义选项列表,限制可用的取值。例如,一个配置类可以定义支持的日志级别。
enum LogLevel {
DEBUG,
INFO,
WARNING,
ERROR
}
class Logger {
private LogLevel logLevel;
public void setLogLevel(LogLevel logLevel) {
this.logLevel = logLevel;
}
public void log(String message, LogLevel level) {
if (level.ordinal() >= logLevel.ordinal()) {
// 执行日志记录逻辑
}
}
}
Logger logger = new Logger();
logger.setLogLevel(LogLevel.INFO);
logger.log("This is an informational message.", LogLevel.INFO);
- 简化条件逻辑:枚举可用于简化条件逻辑。它可以代替使用多个布尔标志或复杂的if-else语句的情况。
enum UserType {
ADMIN,
MODERATOR,
USER
}
class User {
private UserType userType;
public boolean hasAdminAccess() {
return userType == UserType.ADMIN || userType == UserType.MODERATOR;
}
}
User user = new User();
user.setUserType(UserType.MODERATOR);
if (user.hasAdminAccess()) {
// 执行管理员权限逻辑
}
这些案例只是Java枚举在项目中的一些常见用法示例。实际上,枚举在许多不同领域和应用程序中都有广泛的应用,可以根据具体需求自由发挥其灵活性和功能。
Java 枚举的高级主题
- 序列化和反序列化:枚举类型默认是可序列化的,可以将枚举对象写入流或从流中读取。枚举的序列化和反序列化是安全的,因为枚举值是由JVM保证唯一性的。
enum Size {
SMALL, MEDIUM, LARGE
}
// 序列化
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("enum.ser"))) {
out.writeObject(Size.SMALL);
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("enum.ser"))) {
Size size = (Size) in.readObject();
System.out.println(size); // 输出:SMALL
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
- 枚举的比较:枚举可以使用
==
运算符进行比较,因为每个枚举值都是唯一的实例。可以使用equals()
方法进行相等性比较,以及使用compareTo()
方法进行排序。
enum Size {
SMALL, MEDIUM, LARGE
}
Size size1 = Size.SMALL;
Size size2 = Size.SMALL;
System.out.println(size1 == size2); // 输出:true
System.out.println(size1.equals(size2)); // 输出:true
System.out.println(size1.compareTo(size2)); // 输出:0
- 枚举的顺序:每个枚举类型都有一个默认的顺序,可以使用
ordinal()
方法获取枚举值的顺序值。请注意,枚举的顺序值从0开始。
enum Size {
SMALL, MEDIUM, LARGE
}
System.out.println(Size.SMALL.ordinal()); // 输出:0
System.out.println(Size.MEDIUM.ordinal()); // 输出:1
System.out.println(Size.LARGE.ordinal()); // 输出:2
- 自定义方法和字段:枚举可以具有自定义的方法和字段,可以为每个枚举值定义不同的行为和属性。这使得枚举更加灵活和强大。
enum Size {
SMALL("S"), MEDIUM("M"), LARGE("L");
private String abbreviation;
Size(String abbreviation) {
this.abbreviation = abbreviation;
}
public String getAbbreviation() {
return abbreviation;
}
}
Size size = Size.SMALL;
System.out.println(size.getAbbreviation()); // 输出:S
- 枚举的反射:通过反射,可以获取枚举的值、名称和其他元数据。可以使用
values()
方法获取所有的枚举值,使用valueOf()
方法通过名称获取枚举值。
enum Size {
SMALL, MEDIUM, LARGE
}
Size[] values = Size.values();
for (Size size : values) {
System.out.println(size);
}
Size size = Size.valueOf("SMALL");
System.out.println(size); // 输出:SMALL
- 枚举的线程安全性:Java枚举在多线程环境中是线程安全的。这是因为Java枚举的实例是在类加载期间创建的,并且在整个应用程序中是唯一的。当多个线程同时访问枚举实例时,不会发生竞争条件或数据不一致的情况。
public enum SingletonEnum {
INSTANCE;
private SingletonEnum() {
// 构造函数
}
public void doSomething() {
// 枚举实例的操作
}
}
在上述代码中,我们创建了一个名为SingletonEnum
的枚举类型。该枚举只有一个实例INSTANCE
,通过SingletonEnum.INSTANCE
即可访问。
由于枚举实例是在类加载期间创建的,并且在整个应用程序中是唯一的,因此多个线程访问SingletonEnum.INSTANCE
时,都会访问同一个实例。这就确保了线程安全性,无需额外的同步机制或线程安全的处理。
可以在多个线程中同时调用SingletonEnum.INSTANCE.doSomething()
方法,而不会发生线程安全问题。
这说明Java枚举是线程安全的,不需要开发人员采取额外的措施来确保其在多线程环境中的安全性。枚举的唯一实例性质和不可变性保证了在多线程环境中的正确性和可靠性。