跟js不同
java没有js的隐形转换,int和Boolean不相容,输出是双引号
以下是一个简单的Java类示例代码,它包含一个名为Person的类,该类具有两个成员变量和一个方法:¹
public class Person {
String name;
int age;
public void sayHello() {
System.out.println("Hello, my name is " + name + ", I'm " + age + " years old.");
}
}
类跟对象
这个类定义了一个人的基本属性:姓名和年龄。它还定义了一个方法,用于向控制台输出问候语。要使用这个类,您需要在另一个Java文件中创建一个对象并设置其属性,然后调用该对象的方法。例如:
public class Main {
public static void main(String[] args) {
Person person = new Person();
person.name = "John";
person.age = 30;
person.sayHello();
}
}
public class Person {
private String name; // 未初始化,默认值为null
private int age; // 未初始化,默认值为0
public void sayHello() {
// 若name为null,拼接字符串时会显示"null",而非报错,但逻辑不合理
System.out.println("Hello, my name is " + name + ", I'm " + age + " years old.");
}
}
这将创建一个名为person的新对象,并将其名称设置为"John",年龄设置为30。然后,它将调用该对象的sayHello()方法,该方法将输出以下内容:
Hello, my name is John, I'm 30 years old.
java定义了数组直接输出,会输出散列码,和几重数组,例
String[] x = {"a","b","c"} = [Ljava.lang.String;@7adf9f5f]
得加上Arrays.toString(demo)才是返回数组
多维数组输出 Arrays.deepToString(demo)
java文件编译是先把java源文件编译成class字节码文件,运行javac命令,如果运行没错的话,会生成一个class文件,然后再用java命令执行,后面不用带class
源文件(.java文件),类存在源文件里面,方法存在类里面,语句存在方法中
输出语句print是不带换行,跟着输出,printIn是带换行
System.out.print("Hello ");
String[] args:从控制台接收参数
在Java中,public static是两个关键字的组合。其中,public表示该方法或变量可以被任何类访问,而不仅仅是它所属的类;而static则表示该方法或变量属于类本身,而不是类的实例。因此,可以在不创建类的实例的情况下访问该方法或变量。
例如,下面的代码演示了如何定义一个静态变量和一个静态方法:
public class Main {
// 定义一个静态变量
public static int count = 0;
// 定义一个静态方法
public static void increment() {
count++;
}
public static void main(String[] args) {
// 调用静态方法
increment();
// 访问静态变量
System.out.println(count);
}
}
上面的代码定义了一个名为count的静态变量和一个名为increment()的静态方法。然后,在main()方法中调用了increment()方法并访问了count变量。
类有默认的构造方法,只是不展示
public是访问修饰符,表示该class是公开的。
不写public,也能正确编译,但是这个类将无法从命令行执行
方法名也有命名规则,命名和class一样,但是首字母小写:
js基本类型一开始不赋值 相当于给它指定了默认值。默认值总是0
基本数据类型
- 整数类型: byte(-128到128),short(-32768到32768),int(默认,数据类型32位 2的31平方),long(64位,2的63平方)
- 浮点数类型:float(10的-38次方到10的38次方,4字节,8位有效数字,末尾必须有f或者F),double(10的-308次方到10的308次方,8字节,末尾可以有d,可以不写)
- 字符类型:char (单一的 16 位 Unicode 字符)
- 布尔类型:boolean
注意,char和String是不同的。char是基本数据类型,表示单个字符,定义时使用单引号,只能存储一个字符。String是一个类,表示字符串,定义时使用双引号,可以存储一个或多个字符
整数byte short 取值较小,一般使用int
boolean 类型后面可以跟运算
char 字符型是存单个的字符的
简单说:选类型时,看数据是整数 / 小数 / 字符 / 判断,再根据大小 / 精度选具体类型 —— 够用就行,别浪费空间
-
"表示字符" -
'表示字符' -
\表示字符`` -
\n表示换行符 -
\r表示回车符 -
\t表示Tab -
\u####表示一个Unicode编码的字符boolean isGreater = 5 > 3
定义常量(之后不可重新赋值),常量名通常大写, final int A = 3
包装类
基本数据类型转换为包装类的过程称为装箱,例如把 int 包装成 Integer 类的对象;包装类变为基本数据类型的过程称为拆箱,例如把 Integer 类的对象重新简化为 int。
拿interger举例
Java中的原始数据类型不能为null,因为基本类型就是数据,而不是对象。另一方面,我们所谓的对象只是指向数据存储位置的指针。例如:Integer object = new Integer(3);int number = 3;在这种情况下,object它只是指向值恰好为3的Integer对象的指针。也就是说,在存储变量对象的内存位置,您所拥有的只是对数据实际位置的引用。
java基本类型可以为null吗
Integer和int的区别在于,Integer是int的包装类,int是八大基本数据类型之一(byte,char,short,int,long,float,double,boolean)¹。Integer是类,默认值为null,int是基本数据类型,默认值为0。因此,如果你需要使用一个数字类型的变量,而不需要它为空,那么使用int就可以了。如果你需要一个数字类型的变量,并且需要它为空,那么使用Integer就可以了。
整数运算
在运算过程中,如果参与运算的两个数类型不一致,那么计算结果为较大类型的整型
只能进行加减乘除这些数值计算,不能做位运算和移位运算
浮点数比较
java中字符串和字符是两个不同的概念
js定义数组得先定义数组数量 int [] demo = new int[4] 数量4的数组 也可以先定义好
int [] ns = new int[] {'A',22,12,};
js数组循环
数组作为参数,得和传参一致
public class demo {
public static void printArray(double[] array) {
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
}
public static void main(String[] args) {
printArray(new double[]{3.1, 1, 2, 6, 4, 2});
}
}
int [] sum = {3,4,5,6,7,8};
for( int i:sum) {
System.out.print(i);
}
多维数组 type[][] typeName = new type[typeLength1][typeLength2];
type 可以为基本数据类型和复合数据类型,typeLength1 和 typeLength2 必须为正整数,typeLength1 为行数,typeLength2 为列数。
例如
int[][] a = new int[2][3];
二维数组 a 可以看成一个两行三列的数组。
数组类型
public void setNames(String... names) {
this.names = names;
}
java对象数组
在Java中,定义一个对象数组需要先声明数组变量,然后再为每个元素实例化。所谓的对象数组,就是指包含了一组相关的对象。在使用对象数组时,需要注意:数组一定要先开辟空间,但是因为其是引用数据类型,所以数组里面的每一个对象都是null值,则在使用的时候数组中的每一个对象必须分别进行实例化操作¹²。
例如:
class Student {
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 Demo {
public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student("John", 18);
students[1] = new Student("Jane", 19);
students[2] = new Student("Bob", 20);
for (Student student : students) {
System.out.println(student.getName() + " is " + student.getAge() + " years old.");
}
}
}
这个示例代码定义了一个 Student 类,包含了 name 和 age 两个属性。然后在 Main 类中定义了一个 Student 类型的对象数组,长度为 3,分别存储了三个学生的信息。最后通过 for-each 循环遍历数组并输出每个学生的信息。
反射
Java的反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法 ;对于任意一个对象,都能够调用它的任意一个方法和属性 ,这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的 反射机制。
除了int等基本类型外,Java的其他类型全部都是class(包括interface)
举个生活例子:就像你用遥控器(普通代码)按按钮(调用方法),必须知道遥控器上有哪些按钮。但反射就像你拿着一个 “万能检测器”,对着任何一个电器(类),都能自动查出它有哪些功能(方法),还能强行启动这些功能,哪怕这个功能是隐藏的(私有)。
总结来说:反射让程序在运行时能 “看穿” 类的内部结构,并且灵活操作,不用在写代码的时候就把所有细节固定死。这也是很多框架(比如 Spring)能做到 “配置一下就自动干活” 的核心原因。
-
- 先定义一个普通的类(相当于 “被查的对象”)
public class Person {
// 公有属性(公开信息)
public String name;
// 私有属性(隐藏信息)
private int age;
// 无参构造
public Person() {}
// 有参构造
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 公有方法(公开行为)
public void sayHello() {
System.out.println("大家好,我是" + name);
}
// 私有方法(隐藏行为)
private void secret() {
System.out.println("我的年龄是" + age + "(这是秘密!)");
}
}
- 用反射当 “侦探”,动态操作这个类
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
// --------------------------
// 第一步:拿到类的“身份证”(Class对象)
// --------------------------
// 就像侦探先拿到要查的人的身份证
Class personClass = Class.forName("Person"); // 这里填类的全路径(如果有包要加包名)
// --------------------------
// 第二步:查“构造方法”,动态创建对象
// --------------------------
// 1. 查无参构造,创建一个“空对象”
Constructor noArgsConstructor = personClass.getConstructor();
Person person1 = (Person) noArgsConstructor.newInstance(); // 相当于 new Person()
// 2. 查有参构造,创建带初始值的对象
Constructor hasArgsConstructor = personClass.getConstructor(String.class, int.class);
Person person2 = (Person) hasArgsConstructor.newInstance("张三", 18); // 相当于 new Person("张三", 18)
// --------------------------
// 第三步:查“属性”,并修改值
// --------------------------
// 1. 查公有属性name,直接修改
Field nameField = personClass.getField("name"); // 找到叫“name”的公开属性
nameField.set(person1, "李四"); // 给person1的name赋值“李四”,相当于 person1.name = "李四"
System.out.println("person1的name:" + nameField.get(person1)); // 输出:李四
// 2. 查私有属性age(隐藏信息),强行修改
Field ageField = personClass.getDeclaredField("age"); // 找到叫“age”的私有属性(用getDeclaredField)
ageField.setAccessible(true); // 强制打开权限(相当于撬锁)
ageField.set(person2, 20); // 把person2的age从18改成20,相当于 person2.age = 20(正常代码做不到!)
System.out.println("person2的age(反射查到):" + ageField.get(person2)); // 输出:20
// --------------------------
// 第四步:查“方法”,并调用
// --------------------------
// 1. 查公有方法sayHello,直接调用
Method sayHelloMethod = personClass.getMethod("sayHello"); // 找到叫“sayHello”的公开方法
sayHelloMethod.invoke(person1); // 调用person1的sayHello(),相当于 person1.sayHello() → 输出:大家好,我是李四
// 2. 查私有方法secret(隐藏行为),强行调用
Method secretMethod = personClass.getDeclaredMethod("secret"); // 找到叫“secret”的私有方法
secretMethod.setAccessible(true); // 强制打开权限
secretMethod.invoke(person2); // 调用person2的secret(),相当于 person2.secret() → 输出:我的年龄是20(这是秘密!)
}
}
构造方法
Java中要创造一个类的对象,需要使用 new 关键字。JVM遇到 new 时将会进行类加载,空间分配,自动初始化域等一系列过程,在这里我们只需要知道,new 相当于在你的程序世界中腾出了一块空间,在这块刚刚合适的空间上创造了某一类的对象。
造方法的名称就是类名。构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,构造方法没有返回值(也没有void),调用构造方法,必须用new操作符。
如果一个类没有定义构造方法,编译器会自动为我们生成一个默认构造方法,它没有参数,也没有执行语句,类似这样:
class Person {
public Person() {
}
}
如果既要能使用带参数的构造方法,又想保留不带参数的构造方法,那么只能把两个构造方法都定义出来,如果定义了多个,会通过构造方法的参数数量、位置和类型自动区分
构造方法的重载(多构造方法共存)
一个类可以有多个构造方法,只要参数列表不同(参数数量、类型、顺序不同),这叫 “重载”:
public class Person {
String name;
int age;
// 无参构造
public Person() {
name = "默认";
age = 0;
}
// 只有 name 的构造
public Person(String n) {
name = n;
age = 18; // 默认年龄
}
// 有 name 和 age 的构造
public Person(String n, int a) {
name = n;
age = a;
}
}
// 使用时按需选择
Person p1 = new Person(); // 用无参构造
Person p2 = new Person("李四"); // 用单参构造
Person p3 = new Person("王五", 25); // 用双参构造
继承
// 父类
class 父类名 {
// 父类的属性和方法
}
// 子类继承父类
class 子类名 extends 父类名 {
// 子类的新增属性和方法
// (可选)重写父类的方法
}
// 父类:Animal
class Animal {
String name;
int age;
// 父类的方法
public void eat() {
System.out.println(name + " 在吃东西");
}
}
// 子类:Dog(继承自 Animal)
class Dog extends Animal {
// 子类新增属性
String breed;
// 子类新增方法
public void bark() {
System.out.println(name + " 汪汪叫");
}
// 重写父类的 eat 方法
@Override
public void eat() {
System.out.println(name + " 在吃骨头");
}
}
// 测试
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
dog.name = "旺财"; // 继承自父类的属性
dog.breed = "中华田园犬"; // 子类自身的属性
dog.eat(); // 调用重写后的方法:旺财 在吃骨头
dog.bark(); // 调用子类新增方法:旺财 汪汪叫
}
}
- 单继承限制
Java 不支持多继承(一个子类不能同时继承多个父类),但支持 多层继承(子类 → 父类 → 祖父类...)。
-
- 访问权限控制
子类只能访问父类的 非私有成员(public、protected、默认访问权限),private 成员只能在父类内部访问。
class Animal {
public String name; // 子类可访问
protected int age; // 子类可访问
String color; // 同包子类可访问
private String id; // 子类不可访问
}
-
- 方法重写(Override)
子类可以重写父类的方法,需满足以下条件:
- 方法名、参数列表、返回值类型必须与父类完全一致(返回值可兼容子类类型,即 “协变返回”)。
- 子类方法的访问权限 不能低于 父类(如父类是
protected,子类可改为public,不能改为private)。 - 父类
final方法不能被重写。 - 用
@Override注解显式声明重写(可选,但推荐,编译器会校验合法性)。
-
- 构造器的继承规则
- 子类 不能继承 父类的构造器,但子类构造器必须先调用父类的构造器(默认或显式)。
- 若子类构造器未显式调用父类构造器,编译器会自动插入
super();(调用父类无参构造器)。 - 若父类没有无参构造器,子类必须显式用
super(参数)调用父类的有参构造器(且必须放在子类构造器第一行)。
class Animal {}
class Dog extends Animal {}
class Puppy extends Dog {} // 允许:多层继承
// class Dog extends Animal, Mammal {} // 错误:不支持多继承
super关键字
我们可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。
this关键字:指向自己的引用。
如果父类的构造器带有参数,则必须在子类的构造器中显式地通过 super 关键字调用父类的构造器并配以适当的参数列表。
super 用于访问父类的成员,主要有 3 种场景:
- 调用父类的构造器:
super(参数)(必须在子类构造器第一行)。 - 调用父类的普通方法:
super.方法名(参数)(当子类重写了父类方法,需访问父类原方法时使用)。 - 访问父类的属性:
super.属性名(当子类属性与父类属性重名时使用)。
class Animal {
String name = "动物";
public void eat() {
System.out.println("动物吃东西");
}
}
class Dog extends Animal {
String name = "狗";
@Override
public void eat() {
super.eat(); // 调用父类的 eat 方法
System.out.println(super.name + " 吃骨头"); // 访问父类的 name 属性
}
}
// 测试
Dog dog = new Dog();
dog.eat();
// 输出:
// 动物吃东西
// 动物 吃骨头
implements多继承
使用 implements 关键字可以变相的使java具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口
public interface A {
public void eat();
public void sleep();
}
public interface B {
public void show();
}
public class C implements A,B {
}
final 关键字
final 可以用来修饰变量(包括类属性、对象属性、局部变量和形参)、方法(包括类方法和对象方法)和类。
final 含义为 "最终的"。
使用 final 关键字声明类,就是把类定义定义为最终类,不能被继承,或者用于修饰方法,该方法不能被子类重写:
修饰符(public/private/default/protected) final 返回值类型 方法名(){//方法体}
你可以把它想象成 “动物世界” 的场景,这是理解继承和转型最经典的方式。
核心概念:继承关系
首先,我们需要一个父类和一个子类。
java
运行
// 父类:动物
class Animal {
public void eat() {
System.out.println("动物在吃东西");
}
}
// 子类:狗,继承自动物
class Dog extends Animal {
public void eat() { // 重写了父类的方法
System.out.println("狗在吃骨头");
}
public void bark() { // 子类特有的方法
System.out.println("狗在汪汪叫");
}
}
这里,Dog 是 Animal 的子类。这意味着:
- 一只
Dog是一个Animal(is-a 关系)。 Dog继承了Animal的所有属性和方法(比如eat())。Dog还可以有自己特有的属性和方法(比如bark())。
一、向上转型 (Upcasting)
定义: 将一个子类对象赋值给一个父类引用。
通俗解释: 当你看到一只狗时,你可以说 “这是一只动物” 。这个说法是完全正确的,因为狗确实是动物的一种。
代码示例:
public class Main {
public static void main(String[] args) {
// 1. 创建一个子类对象
Dog myDog = new Dog();
// 2. 向上转型:把 Dog 对象赋值给 Animal 类型的引用
Animal myAnimal = myDog; // 或者直接写成 Animal myAnimal = new Dog();
// 3. 现在,我们可以通过 myAnimal 引用调用方法
myAnimal.eat(); // 输出:狗在吃骨头
// myAnimal.bark(); // 编译错误!
}
}
发生了什么?
myAnimal是一个Animal类型的引用,但它指向的是一个实实在在的Dog对象。- 当我们调用
myAnimal.eat()时,由于Dog重写了eat方法,所以会执行Dog类的eat方法。这体现了多态性。 - 关键限制: 我们不能通过
myAnimal引用调用Dog类特有的方法,比如bark()。因为在编译器看来,myAnimal的类型是Animal,而Animal类中并没有bark()方法。编译器只认变量声明时的类型,不认它实际指向的对象类型。
向上转型的好处:
-
代码复用和灵活性: 你可以写一个方法,它的参数是
Animal类型。这样,这个方法就可以接收任何Animal的子类对象(如Dog,Cat,Bird等)作为参数,大大提高了代码的通用性。public static void feed(Animal animal) { animal.eat(); } // 调用 feed(new Dog()); // 狗在吃骨头 feed(new Cat()); // 猫在吃鱼 (假设你有一个 Cat 类)
我们来详细探讨一下向上转型和向下转型在日常 Java 开发中的实际作用和应用场景。
向上转型 (Upcasting) 的核心作用:实现多态与代码复用
向上转型的核心价值在于抽象和解耦,它允许你用更通用的父类或接口来编写代码,从而提高代码的灵活性、可扩展性和复用性。这是实现多态(Polymorphism)的关键机制。
- 实现多态,编写通用代码
这是向上转型最根本的用途。通过向上转型,你可以将不同的子类对象统一视为父类对象来处理,从而用同一段代码逻辑处理多种不同的具体类型。
**场景举例:**假设你正在开发一个绘图应用,有 Shape(形状)基类,以及 Circle(圆形)、Rectangle(矩形)、Triangle(三角形)等子类,每个子类都有自己的 draw() 方法实现。
// 父类
class Shape {
public void draw() {
System.out.println("绘制一个形状");
}
}
// 子类
class Circle extends Shape {
@Override
public void draw() {
System.out.println("绘制一个圆形");
}
}
class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("绘制一个矩形");
}
}
如果不使用向上转型,你可能需要为每种形状编写专门的绘制逻辑:
// 不推荐的方式:代码冗余,缺乏扩展性
public void drawCircle(Circle circle) {
circle.draw();
}
public void drawRectangle(Rectangle rectangle) {
rectangle.draw();
}
// 使用时
Circle circle = new Circle();
Rectangle rectangle = new Rectangle();
drawCircle(circle);
drawRectangle(rectangle);
使用向上转型后,你可以编写一个通用的方法:
// 推荐的方式:通用、简洁、可扩展
public void drawShape(Shape shape) { // 接收父类类型
shape.draw(); // 调用子类的实现,体现多态
}
// 使用时
Shape myCircle = new Circle(); // 向上转型
Shape myRectangle = new Rectangle(); // 向上转型
drawShape(myCircle); // 输出:绘制一个圆形
drawShape(myRectangle); // 输出:绘制一个矩形
优势:
- 代码简洁: 只需要一个
drawShape方法,而不是多个。 - 易于扩展: 以后再增加
Triangle、Square等新形状时,无需修改drawShape方法,只需让新类继承Shape并实现draw()即可。这完美契合了开闭原则(对扩展开放,对修改关闭)。
2. 用于集合存储
在 Java 集合(如 List, Set)中,向上转型非常常见。你可以创建一个存储父类对象的集合,然后向其中添加任何子类的对象。
List shapes = new ArrayList<>();
shapes.add(new Circle());
shapes.add(new Rectangle());
shapes.add(new Triangle()); // 假设已存在
// 遍历集合,统一处理
for (Shape shape : shapes) {
shape.draw(); // 自动调用对应子类的 draw 方法
}
这使得集合的处理变得非常灵活和统一。
二、向下转型 (Downcasting)
定义: 将一个父类引用(这个引用实际上指向的是一个子类对象)强制转换回子类类型。
通俗解释: 你看到一个动物(myAnimal),并且你通过某些线索(比如它在摇尾巴、汪汪叫)判断出 “这只动物其实是一条狗” 。于是,你就把它当作一条狗来对待,可以让它做狗特有的事情,比如吠叫。
代码示例:
public class Main {
public static void main(String[] args) {
// 1. 先进行向上转型
Animal myAnimal = new Dog();
myAnimal.eat(); // 输出:狗在吃骨头
// 2. 尝试向下转型
// Dog myDog = myAnimal; // 编译错误!不能直接把 Animal 赋给 Dog
// 3. 正确的向下转型:使用强制类型转换符 (子类类型)
Dog myDog = (Dog) myAnimal;
// 4. 现在,我们可以调用 Dog 类特有的方法了
myDog.bark(); // 输出:狗在汪汪叫
myDog.eat(); // 输出:狗在吃骨头 (同样是多态)
}
}
发生了什么?
myAnimal这个Animal类型的引用,实际上指向的是一个Dog对象。这是向下转型能够成功的前提条件。- 我们使用
(Dog)这个强制类型转换符,明确告诉编译器:“别把它当成普通的Animal了,我知道它的真实身份是Dog,请把它转换回来。” - 转换成功后,
myDog就是一个Dog类型的引用,指向那个Dog对象。我们就可以用它调用Dog类的所有方法,包括继承来的和它自己特有的。
向下转型的风险与安全:
向下转型是有风险的!如果父类引用指向的对象不是你要转换的那个子类类型,就会抛出 ClassCastException(类型转换异常)。
Animal myAnimal = new Animal(); // myAnimal 指向一个纯粹的 Animal 对象
Dog myDog = (Dog) myAnimal; // 运行时会抛出 ClassCastException!
// 因为一个普通的动物不一定是狗。
如何安全地进行向下转型?
在进行向下转型之前,最好先用 instanceof 关键字来判断一下。
instanceof 的作用是:判断一个对象是否是某个类(或接口)的实例。
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Dog();
// 使用 instanceof 进行安全检查
if (myAnimal instanceof Dog) {
System.out.println("myAnimal 确实是一只 Dog,可以安全转换!");
Dog myDog = (Dog) myAnimal;
myDog.bark();
} else if (myAnimal instanceof Cat) {
System.out.println("myAnimal 是一只 Cat。");
// ... 可以在这里转换为 Cat
}
}
}
总结:
| 特性 | 向上转型 (Upcasting) | 向下转型 (Downcasting) |
|---|---|---|
| 方向 | 子类 -> 父类 | 父类 -> 子类 |
| 语法 | 自动转换,无需显式操作 | 需要强制类型转换 (子类类型) |
| 安全性 | 安全,永远不会抛出异常 | 不安全,可能抛出 ClassCastException |
| 前提 | 存在继承关系即可 | 父类引用必须指向一个子类对象 |
| 访问范围 | 只能访问父类中定义的成员(方法和属性) | 可以访问子类特有的成员 |
| 比喻 | 说 “狗是动物” | 说 “这个动物是狗” |
| 常见用途 | 实现多态、提高代码通用性 | 当需要使用子类特有的功能时 |
向下转型 (Downcasting) 的核心作用:恢复子类特性
向下转型的核心价值在于当你需要访问子类特有的属性或方法时,将一个已经向上转型的父类引用 “还原” 回它原本的子类类型。
1. 处理异质集合中的特定对象
当你有一个存储父类对象的集合(如 List),而你需要对其中某一个特定子类的对象执行它独有的操作时,就必须进行向下转型。
**场景举例:**假设 Circle 类有一个特有的方法 getRadius(),而 Rectangle 有 getArea()。
class Circle extends Shape {
private double radius;
// ... 构造函数等
@Override
public void draw() { /* ... */ }
public double getRadius() { return radius; } // 子类特有方法
}
class Rectangle extends Shape {
private double width, height;
// ... 构造函数等
@Override
public void draw() { /* ... */ }
public double getArea() { return width * height; } // 子类特有方法
}
当遍历 List 时:
List shapes = new ArrayList<>();
shapes.add(new Circle(5.0));
shapes.add(new Rectangle(4.0, 6.0));
for (Shape shape : shapes) {
shape.draw(); // 通用操作,无需转型
// 针对特定类型的特殊操作
if (shape instanceof Circle) {
// 向下转型
Circle circle = (Circle) shape;
System.out.println("圆形的半径是: " + circle.getRadius());
} else if (shape instanceof Rectangle) {
// 向下转型
Rectangle rectangle = (Rectangle) shape;
System.out.println("矩形的面积是: " + rectangle.getArea());
}
}
注意:在向下转型前,使用 instanceof 进行类型检查是至关重要的,这可以避免 ClassCastException 运行时异常。
重写(Override)和重载(overload)
重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!
重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
class Animal{
public void move(){
System.out.println("动物可以移动");
}
}
class Dog extends Animal{
public void move(){
System.out.println("狗可以跑和走");
}
public void bark(){
System.out.println("狗可以吠叫");
}
}
public class TestDog{
public static void main(String args[]){
Animal a = new Animal(); // Animal 对象
Animal b = new Dog(); // Dog 对象
a.move();// 执行 Animal 类的方法
b.move();//执行 Dog 类的方法
b.bark(); // 报错,因为b的引用类型Animal没有bark方法。
}
}
overload
重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。根据不同参数调用不同方法
每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。
public class Demo {
public int test(){
System.out.println("test1");
return 1;
}
public void test(int a){
System.out.println("test2");
}
//以下两个参数类型顺序不同
public String test(int a,String s){
System.out.println("test3");
return "returntest3";
}
public String test(String s,int a){
System.out.println("test4");
return "returntest4";
}
public static void main(String[] args){
Demo o = new Demo();
System.out.println(o.test());
o.test(1);
System.out.println(o.test(1,"test3"));
System.out.println(o.test("test4",1));
}
}
//test1
//1
//test2
//test3
//returntest3
//test4
//returntest4
多态
一句话概括多态
多态是指同一个行为在不同对象上会产生不同的表现形式。
换句话说,当你调用一个方法时,这个方法会根据调用它的对象的实际类型来执行不同的逻辑。
多态的实现条件
在 Java 中,要实现多态,必须满足以下三个条件:
- 继承:存在一个父类和一个或多个子类。
- 方法重写(Override) :子类重写了父类中的某个方法。
- 向上转型:使用一个父类类型的引用指向一个子类类型的对象。
多态的经典例子
我们继续使用之前的 Animal 和 Dog、Cat 的例子,这是解释多态最经典的场景。
// 1. 继承:Animal 是父类
class Animal {
public void makeSound() {
System.out.println("动物发出了声音");
}
}
// Dog 是子类,继承自 Animal
class Dog extends Animal {
// 2. 方法重写:Dog 重写了 makeSound 方法
@Override
public void makeSound() {
System.out.println("狗在汪汪叫:Woof! Woof!");
}
}
// Cat 是另一个子类,继承自 Animal
class Cat extends Animal {
// 2. 方法重写:Cat 也重写了 makeSound 方法
@Override
public void makeSound() {
System.out.println("猫在喵喵叫:Meow! Meow!");
}
}
public class Main {
public static void main(String[] args) {
// 3. 向上转型:父类引用指向子类对象
Animal myAnimal;
myAnimal = new Dog(); // myAnimal 现在指向一个 Dog 对象
myAnimal.makeSound(); // 输出:狗在汪汪叫:Woof! Woof!
myAnimal = new Cat(); // myAnimal 现在指向一个 Cat 对象
myAnimal.makeSound(); // 输出:猫在喵喵叫:Meow! Meow!
myAnimal = new Animal(); // myAnimal 指向一个纯粹的 Animal 对象
myAnimal.makeSound(); // 输出:动物发出了声音
}
}
发生了什么?
- 我们有一个名为
myAnimal的Animal类型引用。 - 这个引用可以指向
Animal类的任何子类对象(Dog或Cat)。 - 当我们调用
myAnimal.makeSound()时,Java 虚拟机(JVM)会在运行时动态地确定myAnimal所指向的对象的实际类型,并调用该类型中重写的makeSound方法。 - 因此,同样的方法调用
myAnimal.makeSound(),在不同的时间点,由于myAnimal指向的对象不同,会产生不同的输出结果。这就是多态的魅力。
多态的核心原理:动态绑定(Dynamic Binding)
多态的实现依赖于 Java 的动态绑定机制,也叫运行时绑定。
- 编译时:编译器只知道
myAnimal的声明类型是Animal。它会检查Animal类中是否存在makeSound方法。如果存在,编译就通过。 - 运行时:当程序执行到
myAnimal.makeSound()时,JVM 会去查看myAnimal这个引用实际指向的对象(是Dog还是Cat)。然后,它会找到这个实际对象所属类中重写的makeSound方法并执行它。
这种绑定过程发生在程序运行时,而不是编译时,所以称为动态绑定。
注意:如果一个方法被 static、final 或 private 关键字修饰,那么它将采用静态绑定(编译时绑定),无法实现多态。
多态的优势
多态是面向对象编程(OOP)的三大核心特性之一(另外两个是封装和继承),它带来了巨大的好处:
-
代码复用与扩展性:
- 你可以编写通用的代码来操作父类对象,这些代码可以无缝地处理所有子类对象。
- 当需要添加新的功能(比如增加一个
Bird类)时,你只需要创建一个新的Animal子类并实现makeSound方法,而无需修改任何已有的、处理Animal对象的通用代码。这完美符合开闭原则(对扩展开放,对修改关闭)。
// 新增一个 Bird 类 class Bird extends Animal { @Override public void makeSound() { System.out.println("小鸟在唱歌:Chirp! Chirp!"); } } // 主方法中无需修改,直接使用 public class Main { public static void main(String[] args) { Animal myAnimal = new Bird(); myAnimal.makeSound(); // 输出:小鸟在唱歌:Chirp! Chirp! // 可以将所有动物放入一个数组中统一处理 Animal[] animals = {new Dog(), new Cat(), new Bird()}; for (Animal a : animals) { a.makeSound(); // 自动调用各自的方法 } } } -
代码简化与灵活性:
- 多态允许你用一个统一的接口(父类的方法)来表示不同的行为,减少了代码的复杂度。
- 你可以用一个父类类型的参数来接收任何子类对象,使得方法的参数类型更加灵活。
// 一个通用的方法,可以喂任何动物 public static void feed(Animal animal) { System.out.println("正在喂食..."); animal.makeSound(); // 动物会根据自己的种类做出反应 } // 调用 feed(new Dog()); // 正在喂食... 狗在汪汪叫 feed(new Cat()); // 正在喂食... 猫在喵喵叫
多态的局限性
-
只能调用父类中声明的方法:通过父类引用,你只能调用父类中已经定义的方法。即使子类有自己特有的方法(比如
Dog的bark()),你也无法通过Animal类型的引用直接调用。要调用子类特有的方法,必须进行向下转型。Animal myDog = new Dog(); myDog.makeSound(); // OK // myDog.bark(); // 编译错误!Animal 类没有 bark 方法 if (myDog instanceof Dog) { Dog dog = (Dog) myDog; dog.bark(); // OK,向下转型后可以调用 } -
属性不具备多态性:多态只适用于方法,不适用于属性(成员变量)。当通过父类引用访问一个被子类重写的属性时,访问到的是父类中定义的属性。
class Animal { String name = "动物"; } class Dog extends Animal { String name = "狗"; } Animal a = new Dog(); System.out.println(a.name); // 输出:动物 (访问的是父类的属性)
总结
- 多态:同一个方法调用,作用在不同的对象上,产生不同的结果。
- 实现三要素:继承、方法重写、父类引用指向子类对象。
- 核心原理:运行时的动态绑定。
- 主要优势:提高代码的复用性、扩展性和灵活性,符合开闭原则。
- 常见误区:多态不适用与
static,final,private方法和成员变量。
抽象
只定 “要做什么” 的规矩,不定 “具体怎么做” 的细节
就像生活里只说 “要吃饭”,但不管是煮米饭、煮面条还是点外卖,具体做法留给别人去定。
一、抽象类:像 “通用模板”
比如你想开一家 “奶茶店”,先定一个「奶茶店模板」(抽象类):
// 抽象类 = 奶茶店通用模板
abstract class 奶茶店 {
// 通用操作(有具体做法,不用改)
public void 收钱() {
System.out.println("收顾客15块钱");
}
// 必须做,但做法不一样(只定规矩,没具体做法)
public abstract void 做奶茶(); // 抽象方法:只有“要做奶茶”的规矩,没“怎么做”
}
这个 “模板”(抽象类)有两个特点:
-
不能直接用:你没法直接拿这个模板开店(不能
new 奶茶店()),因为 “做奶茶” 的细节没定; -
必须继承补全:得创建具体的奶茶店(子类),把 “做奶茶” 的细节补上:
-
可包含 抽象方法(无方法体,需子类实现)和 非抽象方法(有方法体);
-
有抽象方法的类,必须声明为抽象类(反之,抽象类可无抽象方法);
-
子类继承抽象类时,必须重写所有抽象方法(除非子类也是抽象类);
-
可包含成员变量、构造方法(用于子类初始化)。
// 具体的蜜雪冰城(继承模板,补全细节) class 蜜雪冰城 extends 奶茶店 { @Override public void 做奶茶() { System.out.println("泡红茶+加植脂末+加冰"); // 补全蜜雪冰城的做法 } } // 具体的喜茶(另一种做法) class 喜茶 extends 奶茶店 { @Override public void 做奶茶() { System.out.println("现萃茶底+鲜奶+芝士顶"); // 补全喜茶的做法 } }
简单说:抽象类是 “有部分现成内容 + 部分待补内容” 的模板,子类必须把 “待补的事” 做完才能用。
抽象方法
- 定义
用 abstract 修饰的方法,只有方法签名,没有方法体(以分号结尾),必须声明在抽象类或接口中。
- 核心规则
- 不能用
private、final、static修饰(这些关键字会阻止子类重写); - 子类必须重写所有抽象方法(除非子类是抽象类)。
接口
Java 的接口,用最大白话讲就是:一份 “能力承诺书”/“行为清单” —— 只写 “你要能做这些事”,但完全不管 “你具体怎么做这些事”,谁接了这份清单,就得把清单里的事都落地,还能同时接多份清单。
- 接口的核心语法规范
-
声明语法关键字为
interface,接口成员有严格的默认修饰符:- 方法:默认
public abstract(JDK 8 后支持default默认方法、static静态方法,可带方法体); - 变量:默认
public static final(必须显式初始化,且不可修改); - 无构造方法(接口不实例化)。
public interface Flyable { // 常量(public static final) int MAX_SPEED = 100; // 抽象方法(public abstract) void fly(); // JDK8+ 默认方法(可带实现,供实现类继承/重写) default void stop() { System.out.println("停止飞行"); } // JDK8+ 静态方法(属于接口,不被实现类继承) static void validateSpeed(int speed) { if (speed > MAX_SPEED) throw new IllegalArgumentException(); } } - 方法:默认
-
实现规则类通过
implements关键字实现接口,需满足:- 非抽象类必须实现接口中所有抽象方法,且方法访问修饰符需为
public(不能缩小权限); - 一个类可实现多个接口(用逗号分隔),弥补 Java 单继承的局限;
- 实现类可重写接口的
default方法,若实现多个接口存在同名default方法,必须显式重写以解决冲突。
// 实现单个接口 public class Plane implements Flyable { @Override public void fly() { System.out.println("飞机以" + MAX_SPEED + "km/h飞行"); } } // 实现多个接口 public class Duck implements Flyable, Swimmable { @Override public void fly() { /* 实现 */ } @Override public void swim() { /* 实现 */ } // 若Flyable和Swimmable有同名default方法,需重写 @Override public void stop() { /* 自定义实现 */ } } - 非抽象类必须实现接口中所有抽象方法,且方法访问修饰符需为
接口继承接口可通过extends继承多个接口(接口间支持多继承),子接口会继承父接口的所有抽象方法、常量和default方法:
```
public interface Movable {
void move();
}
// 子接口继承多个父接口
public interface FlyableMovable extends Flyable, Movable {
void hover(); // 新增抽象方法
}
```
- 三、接口的核心特性
- 抽象性:核心是对 “行为” 的抽象,仅定义 “做什么”,不定义 “怎么做”,体现面向抽象编程思想;
- 契约性:接口是实现类与调用方的契约,调用方仅依赖接口类型,无需关注具体实现,降低耦合;
- 多态性:接口可作为引用类型,指向其任意实现类的实例,符合 “里氏替换原则”;
- 无实例化:接口不能通过
new实例化,仅能被类实现或被其他接口继承; - 权限控制:接口成员仅支持
public权限(JDK 9 后支持private方法,仅用于接口内部逻辑封装)。
封装
封装就是:把类里的核心数据(比如年龄、用户名)“藏起来” 不让外部直接改,只留几个 “规矩的入口”(方法),外部只能通过这些入口访问 / 修改数据,还能在入口里加 “校验规矩”(比如年龄不能是负数) 。
-
举个简单代码例子(对比 “没封装” 和 “封装”)
-
没封装的情况(相当于手机拆了壳,零件全露外面)
public class User {
// 数据直接暴露,外部能随便改
int age;
}
// 外部调用:想怎么改就怎么改,全乱了
public class Test {
public static void main(String[] args) {
User user = new User();
user.age = -5; // 年龄设成-5,明显不合理,但没人拦着
System.out.println(user.age); // 输出-5,数据全乱了
}
}
- 封装后的情况(相当于手机封好壳,只留操作入口)
public class User {
// 第一步:把数据藏起来(用private修饰,外部碰不到)
private int age;
// 第二步:留“读数据的入口”(get方法)
public int getAge() {
return this.age; // 外部想知道年龄,只能走这个口
}
// 第三步:留“改数据的入口”(set方法),并加校验规矩
public void setAge(int age) {
if (age < 0 || age > 150) { // 规矩:年龄不能是负数/超过150
System.out.println("年龄不对!");
return;
}
this.age = age; // 符合规矩才让改
}
}
// 外部调用:只能走入口,数据不会乱
public class Test {
public static void main(String[] args) {
User user = new User();
user.setAge(-5); // 想改负数,入口直接拦着,输出“年龄不对!”
user.setAge(25); // 符合规矩,能改
System.out.println(user.getAge()); // 输出25,数据是对的
}
}
- 封装的核心好处(为啥要搞封装?)
- 保护数据不被瞎改:就像手机封壳,避免外部乱操作导致数据出错(比如年龄设成 - 5);
- 不用懂内部咋干活:你按手机电源键不用懂芯片原理,调用 setAge () 不用懂 “为啥要校验 0-150”,只用按规矩用就行;
- 改内部逻辑不影响外部:比如以后想把年龄校验改成 “0-120”,只改 setAge () 里的规矩就行,外部调用的代码不用动(比如还是写 user.setAge (25))。
- 总结
Java 封装就一句话:把内部的核心数据锁起来,只留带规矩的 “小门” 让外面进,既防乱改,又省事儿。
泛型
用生活化的例子 + 极简代码把泛型讲透,全程不绕技术词,核心就讲清 “泛型到底解决啥问题、怎么用”:
一、先看生活里的 “泛型”—— 最直观的类比
你平时收的快递盒子就是典型:
- 没有 “泛型” 的盒子:盒子上啥都不标,里面可能装手机、零食、衣服,你拆盒前根本不知道是啥,拿的时候还可能拿错(比如想拿手机,结果掏出一包薯片);
- 有 “泛型” 的盒子:盒子上明确标着 “仅装手机”“仅装零食”,你拆盒前就知道里面是啥,而且装的时候也只能装指定的东西(想把零食塞进 “仅装手机” 的盒子,快递员直接不让装)。
Java 里的泛型,本质就是给 “容器 / 类 / 方法” 贴个 “类型标签”:提前规定它只能装 / 处理什么类型的数据,既避免装错,也避免用的时候拿错。
二、先看 “没有泛型的坑”—— 为啥需要泛型?
比如用 Java 最常用的容器ArrayList(相当于一个 “无标签快递盒”):
public class Test {
public static void main(String[] args) {
// 没泛型的ArrayList:啥类型都能装
ArrayList box = new ArrayList();
box.add("苹果"); // 装字符串
box.add(123); // 装数字
box.add(true); // 装布尔值
// 取数据时:必须手动“强转”,还容易出错
String fruit = (String) box.get(0); // 能取到,没问题
String num = (String) box.get(1); // 运行时直接报错!因为123是数字,转不成字符串
}
}
问题核心:
- 装的时候无限制,啥都能塞,数据杂乱;
- 取的时候要手动 “强转类型”,容易转错,而且错误要到运行时才发现(写代码时看不出来)。
三、有泛型的解决 —— 给盒子贴 “类型标签”
给ArrayList加个 “标签”,规定它只能装某一种类型:
public class Test {
public static void main(String[] args) {
// 泛型写法:<String> 表示这个盒子只能装字符串
ArrayList<String> strBox = new ArrayList<String>();
strBox.add("苹果"); // 能装,符合标签要求
// strBox.add(123); // 编译时直接报错!根本不让装数字(不用等运行才发现)
// 取数据时:不用强转,直接就是String类型
String fruit = strBox.get(0);
System.out.println(fruit); // 输出:苹果
// 再建一个“仅装数字”的盒子
ArrayList<Integer> numBox = new ArrayList<Integer>();
numBox.add(123);
// numBox.add("香蕉"); // 同样编译报错,不让装字符串
}
}
核心变化:
- 装的时候:不符合 “标签类型” 的东西,编译时就拦着,根本装不进去;
- 取的时候:不用手动强转,直接就是标签指定的类型,不会错。
四、泛型的核心:给类型 “留个位置,用的时候再填”
泛型的本质是 “类型参数化” —— 通俗说就是:写类 / 方法的时候,不固定死要处理的类型(比如不固定只能装 String),而是留一个 “占位符”(比如用<T>表示),等用这个类 / 方法的时候,再把占位符换成具体类型(比如 String、Integer)。
举个自定义泛型类的例子(更易理解)
比如自己写一个 “快递盒” 类,用泛型规定它能装的东西:
// 自定义泛型类:<T> 就是类型占位符(T是随便起的,也可以叫E、K、V)
class ExpressBox<T> {
// 盒子里的物品,类型是占位符T
private T item;
// 装东西:参数类型是T
public void put(T item) {
this.item = item;
}
// 取东西:返回类型是T
public T get() {
return item;
}
}
// 用的时候给占位符填具体类型
public class TestBox {
public static void main(String[] args) {
// 1. 填String:这个盒子只能装字符串
ExpressBox<String> strBox = new ExpressBox<>();
strBox.put("手机");
String phone = strBox.get(); // 不用强转
// 2. 填Integer:这个盒子只能装数字
ExpressBox<Integer> numBox = new ExpressBox<>();
numBox.put(666);
int num = numBox.get(); // 不用强转
// 3. 填自定义类型:比如User类
ExpressBox<User> userBox = new ExpressBox<>();
userBox.put(new User("张三", 25));
User user = userBox.get(); // 不用强转
}
}
// 随便定义一个User类(仅用于示例)
class User {
String name;
int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
这个例子里,<T>就像快递盒上的 “空白标签”,用的时候填 “String”“Integer”“User”,盒子就只能装对应类型的东西。
五、泛型的核心好处(为啥要搞泛型?)
- 防错:编译时就检查类型,不让装错数据(比如数字塞进字符串盒子),避免运行时报错;
- 省事:取数据不用手动强转,代码更简洁;
- 复用:一个泛型类 / 方法能处理多种类型,不用写多个重复的类(比如不用写 StringBox、IntegerBox、UserBox,一个 ExpressBox 就够)。
六、简单提一下泛型方法(额外补充,易懂版)
如果只是某个方法需要泛型,不用给整个类加泛型,直接给方法加:
// 泛型方法:<T> 写在返回值前面,代表这个方法有个类型占位符T
public static <T> void print(T thing) {
System.out.println("打印:" + thing);
}
// 调用时自动匹配类型
print("苹果"); // T自动变成String
print(123); // T自动变成Integer
print(true); // T自动变成Boolean
总结
Java 泛型就一句话:给类 / 方法留个 “类型空位”,用的时候指定具体类型,既防装错数据,又不用手动转类型,还能复用代码。
核心记:泛型 = 类型标签 = 占位符,目的是让 “容器 / 方法” 只处理指定类型的数据,少出错、少写重复代码。
泛型和object的区别
// 需求:写一个方法,返回传入的参数(看似简单)
public static Object getThing(Object thing) {
return thing;
}
// 调用时的问题:
String str = (String) getThing("苹果"); // 必须手动强转
Integer num = (Integer) getThing(123); // 必须手动强转
// 致命坑:如果转错类型,编译不报错,运行时直接崩!
String error = (String) getThing(123); // 运行时抛出ClassCastException
泛型看似 “什么类型都行”,但它是 “类型安全的任意类型” ,而 Object 是“无类型检查的任意类型”,核心差异有 3 点:
| 维度 | 只用 Object(无泛型) | 用泛型 |
|---|---|---|
| 类型检查 | 编译时不检查,错把数字转字符串也能过 | 编译时严格检查,类型错直接报错 |
| 类型转换 | 必须手动强转,容易转错 | 自动推断类型,无需手动强转 |
| 代码复用 + 类型安全 | 复用但不安全(运行时崩) | 既复用(一个方法适配所有类型)又安全 |
枚举
一、先看生活里的 “枚举”—— 最直观的类比
你手机里的「天气应用」,只会显示固定的天气类型:晴、雨、阴、雪、多云 —— 不会出现 “晴雨”“雪阴” 这种乱码,也不会出现数字 / 字符串写错的情况(比如把 “晴” 写成 “情”)。
这种「固定且有限的可选值集合」,就是枚举的核心场景:比如一周只有 7 天、一年 12 个月、性别只有男 / 女、订单状态只有待支付 / 已支付 / 已取消……
二、先看 “不用枚举的坑”—— 为啥需要枚举?
如果不用枚举,我们通常用 “常量” 表示固定值,但全是坑:
// 用常量表示订单状态(坑多)
public class OrderStatus {
// 1. 容易写错:比如把1写成0,编译器不提醒
public static final int UNPAID = 1; // 待支付
public static final int PAID = 2; // 已支付
public static final int CANCELED = 3; // 已取消
// 2. 能传任意数字:比如传999,编译器也不拦着
public static void updateStatus(int status) {
System.out.println("订单状态:" + status);
}
}
// 调用时的问题:
public class Test {
public static void main(String[] args) {
OrderStatus.updateStatus(OrderStatus.PAID); // 正常
OrderStatus.updateStatus(999); // 传了个不存在的状态,编译器不报错,运行时逻辑全乱
OrderStatus.updateStatus(1); // 直接写数字,可读性差(谁知道1是待支付?)
}
}
问题核心:
- 取值不固定:能传任意数字 / 字符串,容易传错;
- 可读性差:看数字 1/2/3,不知道对应啥含义;
- 无类型检查:写错值只有运行时才发现问题。
三、枚举的核心:定义 “固定且有限的可选值”
枚举(enum)是 Java 里的一种特殊类型,专门用来定义「固定个数的常量集合」—— 相当于给这些固定值 “起个名字”,而且编译器会严格限制:只能用定义好的值,不能用别的。
第一步:定义枚举(以订单状态为例)
// 用enum关键字定义枚举,里面是所有可选值(逗号分隔)
public enum OrderStatusEnum {
UNPAID, // 待支付
PAID, // 已支付
CANCELED // 已取消
}
这行代码的意思:订单状态只有这 3 种,没有其他可能。
第二步:使用枚举(彻底解决之前的坑)
public class Test {
// 方法参数限定为枚举类型:只能传定义好的3个值
public static void updateStatus(OrderStatusEnum status) {
// 枚举可以直接打印,显示的是名字(可读性强)
System.out.println("订单状态:" + status);
}
public static void main(String[] args) {
updateStatus(OrderStatusEnum.PAID); // 正常:传已支付
// updateStatus(999); // 编译时直接报错!根本不让传数字
// updateStatus("UNPAID"); // 编译时直接报错!根本不让传字符串
// updateStatus(OrderStatusEnum.OTHER); // 编译报错!没有这个值
}
}
核心变化:
- 取值固定:只能用枚举里定义的 UNPAID/PAID/CANCELED,其他值编译器直接拦着;
- 可读性强:看
OrderStatusEnum.PAID就知道是 “已支付”,不用记数字; - 类型安全:错传值编译时就发现,不用等运行时崩。
四、枚举的进阶用法(实用且易懂)
枚举不只是 “列值”,还能加属性、方法,比常量灵活得多:
// 带属性和方法的枚举(比如给状态加“描述”)
public enum OrderStatusEnum {
// 1. 定义值时,传入属性值
UNPAID("待支付", 1),
PAID("已支付", 2),
CANCELED("已取消", 3);
// 2. 枚举的成员变量(私有化,通过getter访问)
private final String desc; // 状态描述
private final int code; // 状态码
// 3. 枚举的构造方法(必须private,默认也是private)
OrderStatusEnum(String desc, int code) {
this.desc = desc;
this.code = code;
}
// 4. 公共方法(获取属性)
public String getDesc() {
return desc;
}
public int getCode() {
return code;
}
// 5. 自定义方法(比如根据code找枚举值)
public static OrderStatusEnum getByCode(int code) {
for (OrderStatusEnum status : values()) { // values()返回所有枚举值数组
if (status.getCode() == code) {
return status;
}
}
throw new IllegalArgumentException("无效的状态码");
}
}
// 调用示例:
public class Test {
public static void main(String[] args) {
// 获取枚举的属性
System.out.println(OrderStatusEnum.PAID.getDesc()); // 输出:已支付 //注意.PAID. 定义时,已经调用了构造方法
System.out.println(OrderStatusEnum.PAID.getCode()); // 输出:2
// 根据code找枚举值
OrderStatusEnum status = OrderStatusEnum.getByCode(1);
System.out.println(status); // 输出:UNPAID
}
}
五、枚举的核心特点(通俗版)
-
枚举是特殊的类:每个枚举值都是这个枚举类的「唯一实例」(比如 UNPAID 是 OrderStatusEnum 的一个对象);
-
构造方法必须私有:不能手动 new 枚举对象(比如
new OrderStatusEnum()会报错),保证枚举值只有定义的那几个; -
自带实用方法:
values():返回所有枚举值的数组(遍历枚举常用);valueOf(String name):根据名字找枚举值(比如OrderStatusEnum.valueOf("PAID")返回 PAID);ordinal():返回枚举值的下标(UNPAID 是 0,PAID 是 1,CANCELED 是 2)。
六、什么时候用枚举?(实用场景)
只要满足「值是固定的、有限的」,就用枚举:
- 状态类:订单状态、支付状态、物流状态;
- 分类类:性别、星期、月份、季节;
- 配置类:接口返回码(成功 / 失败 / 参数错误)、权限类型。
总结
Java 枚举就是「固定值的专属容器」:
- 解决了常量 “取值混乱、可读性差、无类型检查” 的坑;
- 既能像常量一样简单用,又能加属性、方法,灵活度拉满;
- 核心价值:用有限的、明确的、类型安全的取值,替代混乱的常量 / 魔法值。
静态方法,静态字段
静态成员 = 属于 “类” 本身,不用new对象就能直接用(类名.xxx);
普通成员 = 属于 “对象”,必须new出对象才能用(对象名.xxx)。
戳破 “只加 static” 的误区
public class Phone {
// 静态字段
static String BRAND = "小米";
// 普通字段
String color = "黑色";
// 静态方法(加static)
public static void showBrand() {
System.out.println(BRAND); // ✅ 能直接访问静态字段
// System.out.println(color); // ❌ 编译报错:不能直接访问普通字段
// System.out.println(this.color); // ❌ 编译报错:不能用this
// 想访问普通成员?必须先new对象!
Phone p = new Phone();
System.out.println(p.color); // ✅ 这样才可以
}
// 普通方法(不加static)
public void showColor() {
System.out.println(BRAND); // ✅ 能访问静态字段
System.out.println(color); // ✅ 能访问普通字段
System.out.println(this.color); // ✅ 能用this
}
}
// 调用测试
public class Test {
public static void main(String[] args) {
// 静态方法:不用new,类名直接调
Phone.showBrand();
// 普通方法:必须new对象才能调
Phone p = new Phone();
p.showColor();
// 普通方法直接用类名调?❌ 编译报错
// Phone.showColor();
}
}
核心总结
加static只是 “语法标记”,真正的区别是:
- 静态方法是 “类的方法” —— 不依赖任何对象,所以没有 this、不能直接用普通成员;
- 普通方法是 “对象的方法” —— 绑定具体对象,所以能随便用 this、访问所有成员。
一句话记:static不只是一个关键字,它决定了方法 “属于谁”(类 vs 对象),进而决定了它能做什么、怎么用。
包
package(包) 的作用是把不同的 java 程序分类保存,更方便的被其他 java 程序调用。
// 声明当前类属于com.demo.order包
package com.demo.order;
// 导入com.demo.user包下的User类
import com.demo.user.User;
public class Order {
public static void main(String[] args) {
// 直接用导入的User类
User user = new User();
System.out.println(user.name); // ✅ public权限,任意包能访问
// System.out.println(user.age); // ❌ default权限,不同包不能访问
}
}
内部类和匿名类
❶ 成员内部类(相当于 “房子里的固定卧室”)
定义在外部类的成员位置(和字段、方法同级),依赖外部类对象:
// 外部类:房子
public class House {
// 外部类私有字段
private String address = "北京市朝阳区";
// 成员内部类:卧室(依赖House对象)
public class Bedroom {
private String name = "主卧";
// 内部类能直接访问外部类的private字段
public void showInfo() {
System.out.println("卧室" + name + "在" + address);
}
}
// 外部类访问内部类:需要创建内部类对象
public void enterBedroom() {
Bedroom bedroom = new Bedroom();
bedroom.showInfo();
}
}
// 调用示例
public class Test {
public static void main(String[] args) {
// 第一步:创建外部类对象(先造房子)
House house = new House();
// 第二步:创建内部类对象(通过房子造卧室)
House.Bedroom bedroom = house.new Bedroom();
bedroom.showInfo(); // 输出:卧室主卧在北京市朝阳区
// 也可以直接调用外部类的方法间接访问内部类
house.enterBedroom();
}
}
❷ 局部内部类(相当于 “房子里临时搭的小书房”)
定义在外部类的方法 / 代码块里,作用域仅限当前方法,外部无法直接访问:
public class House {
private String address = "上海市浦东新区";
public void createStudyRoom() {
// 局部内部类:定义在方法里,仅限该方法使用
class StudyRoom {
public void show() {
System.out.println("临时书房在" + address);
}
}
// 方法内创建局部内部类对象并使用
StudyRoom studyRoom = new StudyRoom();
studyRoom.show(); // 输出:临时书房在上海市浦东新区
}
}
// 调用
public class Test {
public static void main(String[] args) {
House house = new House();
house.createStudyRoom();
// 无法直接访问StudyRoom:House.StudyRoom sr = ... → 编译报错
}
}
1. 匿名类的核心定义
匿名类是「没有类名的局部内部类」,核心特征:
- 必须继承一个父类 / 实现一个接口;
- 定义的同时必须创建对象(只能用一次);
- 适合 “临时用一次、逻辑简单” 的场景(比如接口回调、事件监听)。
// 定义一个接口(规矩)
interface Coffee {
void showSize();
}
public class Test {
public static void main(String[] args) {
// 匿名类:实现Coffee接口,同时创建对象(没有类名,只用一次)
Coffee mediumCoffee = new Coffee() {
// 实现接口的方法
@Override
public void showSize() {
System.out.println("中杯咖啡");
}
};
mediumCoffee.showSize(); // 输出:中杯咖啡
// 再创建一个匿名类对象(大杯)
Coffee largeCoffee = new Coffee() {
@Override
public void showSize() {
System.out.println("大杯咖啡");
}
};
largeCoffee.showSize(); // 输出:大杯咖啡
}
}
new Coffee() {...}`是 Java 给匿名类设计的 “语法糖”,它其实是三件事一次性做完:
// 匿名类的“等效还原”(写成具名局部内部类)
public class Test {
public static void main(String[] args) {
// 1. 定义一个具名的局部内部类,实现Coffee接口
class MediumCoffee implements Coffee {
@Override
public void showSize() {
System.out.println("中杯咖啡");
}
}
// 2. 创建这个类的对象,赋值给变量
Coffee mediumCoffee = new MediumCoffee();
// 3. 调用方法(和你熟悉的写法一致)
mediumCoffee.showSize();
}
}
常用字符串方法
Java 常用字符串方法(极简 + 单行示例)
| 功能 | 方法 | 单行示例(结果注释) | 关键备注 |
|---|---|---|---|
| 基础 | length() | "Java编程".length() → 6 | 空格 / 中文都算 1 个 |
| isBlank() | " ".isBlank() → true | JDK11+,含全空格判空 | |
| + 拼接 | "Hello"+"World"+2025 → "HelloWorld2025" | 最通用,支持多类型 | |
| 查找 | charAt() | "Java".charAt(1) → 'a' | 下标从 0 开始 |
| indexOf() | "Java编程Java".indexOf("Java",1) → 6 | 找不到返回 - 1 | |
| 截取 / 拆分 | substring() | "HelloWorld".substring(0,5) → "Hello" | end 是开区间,返回新串 |
| split() | "www.baidu.com".split("\.") → ["www","baidu","com"] | 特殊符需转义 \ | |
| 修改替换 | replace() | "Java真香".replace("Java","Python") → "Python 真香" | 不可变,返回新串 |
| toUpperCase() | "java".toUpperCase() → "JAVA" | ||
| strip() | " Java ".strip() → "Java" | JDK11+,支持全角空格 | |
| 判断比较 | equalsIgnoreCase() | "Java".equalsIgnoreCase("java") → true | 不用 == 比内容 |
| contains() | "Java编程".contains("编程") → true | ||
| endsWith() | "test.txt".endsWith(".txt") → true | ||
| 转换 | toCharArray() | "abc".toCharArray() → ['a','b','c'] | |
| String.valueOf() | String.valueOf(123) → "123" | 其他类型转字符串 |
核心避坑点
- String 不可变:修改方法返回新串(如 replace 后原串不变);
- 比较内容用 equals (),== 比的是内存地址;
- 拆分.、| 等分隔符需加 \ 转义。
可变参数
通俗类比
就像去奶茶店点单:
- 普通方法:只能固定点 “3 杯奶茶”(参数个数固定);
- 可变参数方法:可以点 “1 杯、2 杯、N 杯甚至 0 杯奶茶”(参数个数随意)。
基础用法(代码示例)
1. 定义可变参数方法
// 可变参数:int... nums 表示可接收任意个int类型参数
public static int sum(int... nums) {
int total = 0;
// 可变参数本质是数组,遍历方式和数组一样
for (int num : nums) {
total += num;
}
return total;
}
public static void main(String[] args) {
// 调用:传任意个数的参数(0个、1个、多个都可以)
System.out.println(sum()); // 0(传0个参数)
System.out.println(sum(1)); // 1(传1个)
System.out.println(sum(1, 2, 3)); // 6(传多个)
// 也可以直接传数组(可变参数兼容数组)
int[] arr = {4,5,6};
System.out.println(sum(arr)); // 15
}
2. 关键规则(避坑)
① 一个方法只能有一个可变参数,且必须放在参数列表最后:
// 正确:可变参数在最后
public static void print(String tip, int... nums) {}
// 错误:可变参数不在最后
// public static void print(int... nums, String tip) {}
② 可变参数和普通参数不冲突,只是让同类型参数更灵活:
// 示例:固定参数(姓名)+ 可变参数(多门成绩)
public static void showScore(String name, int... scores) {
System.out.println(name + "的成绩:");
for (int s : scores) {
System.out.print(s + " ");
}
}
// 调用
showScore("张三", 80, 90); // 张三的成绩:80 90
showScore("李四"); // 李四的成绩:(传0个成绩也能运行)
核心本质
可变参数只是 Java 的 “语法糖”—— 编译器会自动把传入的多个参数打包成数组,所以在方法内部,处理可变参数和处理数组完全一样,只是调用时不用手动创建数组,更省事。
常见场景
比如工具类方法(如System.out.printf()、String.format()),需要接收不确定个数的参数时,都会用可变参数:
// JDK自带的可变参数示例:printf可传任意个参数
System.out.printf("姓名:%s,年龄:%d", "张三", 25);
总结
- 写法:
类型... 参数名,放在方法参数最后; - 用法:调用时可传 0 个、1 个或多个同类型参数,也可传数组;
- 本质:底层是数组,只是调用更简洁,适配 “参数个数不确定” 的场景。
注解
步骤 1:定义注解(写 “标签模板”)
语法是public @interface 注解名,结合 “元注解”(规定注解的使用规则),比如自定义一个 “接口日志注解”:
import java.lang.annotation.*;
// 元注解1:限定注解能贴在【方法】上(比如不能贴在类、属性上)
@Target(ElementType.METHOD)
// 元注解2:注解保留到运行时(关键!只有这样反射才能读到)
@Retention(RetentionPolicy.RUNTIME)
// 自定义注解:标记方法需要记录日志
public @interface NeedLog {
// 注解的“属性”:相当于标签的“配置项”,可设默认值
String desc() default "接口操作"; // 日志描述,默认值“接口操作”
boolean saveDb() default true; // 是否保存到数据库,默认true
}
关键元注解解释(必须懂) :
| 元注解 | 作用 |
|---|---|
@Target | 限定注解的使用位置:比如METHOD= 方法、TYPE= 类、FIELD= 属性、PARAMETER= 参数 |
@Retention | 注解的 “生命周期”:✅ RUNTIME(运行时):反射能解析(自定义注解几乎都用这个)🔸 CLASS(编译后):字节码里有,但运行时读不到🔸 SOURCE(源码级):编译后就消失(比如@Override) |
注解属性的规则:
- 写法像 “无参数方法”,比如
String desc(); - 可以用
default设默认值,使用时可省略; - 如果只有一个属性且名字是
value,使用时可省略value=(比如@NeedLog("用户登录"))。
步骤 2:使用注解(贴 “标签”)
把自定义的注解贴在需要的地方(这里贴在接口方法上):
public class UserController {
// 贴注解:用默认配置(desc=接口操作,saveDb=true)
@NeedLog
public void getUserList() {
System.out.println("查询用户列表");
}
// 贴注解:自定义配置(desc=用户登录,saveDb=false)// 给注解赋值
@NeedLog(desc = "用户登录", saveDb = false)
public void login(String username) {
System.out.println(username + "登录");
}
}
给方法贴自定义注解,它绝对不会自动执行任何逻辑—— 注解只是 “静态的标记 / 标签”,就像你给商品贴了 “易碎” 标签,商品不会自己 “小心运输”,必须有快递员(解析逻辑)看到标签后,才会执行对应的操作。pring 框架已经帮你写好了通用的解析逻辑:
- 你贴
@GetMapping("/user"),Spring 启动时会扫描所有 Controller,用反射读取这个注解,自动把 “路径” 和 “方法” 绑定; - 你贴
@Transactional,Spring 会通过 AOP 拦截方法,读取注解配置,自动开启 / 提交 / 回滚事务;
这些注解的 “自动执行”,本质是框架替你完成了 “解析注解 + 执行逻辑” 的步骤,不是注解自己会做事。