space——32;0——48;A——65;a——97

一、基本数据类型与数组

1.1 标识符

1.1.1命名规则

  1. 标识符只能由英文字母(区分大小写),数字,下划线,美元符号组成,长度不受限制
  2. 不能以数字开头
  3. 不能使用关键字(Integer,array不是关键字)
  4. 不能使用true,false,null

1.1.2命名规范

  • 类名:首字母大写,驼峰式(如 UserInfo);
  • 方法 / 变量名:首字母小写,驼峰式(如 getUserNamestudentAge);
  • 常量名:全大写,单词间用下划线分隔(如 MAX_VALUE);
  • 包名:全小写,用 . 分隔(如 com.example.utils)。

1.2 基本数据类型

注:

  • float常量后必须要有后缀f或F,或者进行强制类型转换
float x=1.4f;
float y=(float)1.6;
  • 当 + 运算符的两个操作数至少有一个是字符串时,Java 会自动将另一个操作数(无论类型)转换为字符串,然后进行拼接,最终结果为一个新的字符串
  • 若多个 + 连续运算,会从左到右依次执行
String str1="age"+2;
String str2=5+"weight";
System.out.println(22+10+"name");//32name
System.out.println(10+"name"+11+22);//10name22
System.out.println(str1+str2);//age25weight

转义字符

1.3 注释

1.3.1 块注释

/**
* 这是我的第一个代码
* 我是李哥
*/

1.3.2行注释

//。。。

1.4 类型转换运算

精度由低到高:byte   short   char   int   long   float   double

  • 当精度低的变量的值赋给精度高的变量时,系统自动完成数据类型的转换
  • 当精度高的变量的值赋给精度低的变量时,必须使用类型转换运算
int x=(int)12.75;//12

1.5 输入与输出

1.5.1 输入

Scanner使用基本步骤

  1. 创建 Scanner 对象:关联输入流(如控制台输入 System.in);
  2. 调用对应方法读取数据:根据目标数据类型选择 nextXxx() 方法;
  3. 关闭 Scanner:使用完毕后关闭,释放资源(避免内存泄漏)。
import java.util.Scanner;
Scanner scanner=new Scanner(System.in);
byte b=scanner.nextByte();
int i=scanner.nextInt();
long l=scanner.nextLong();
float f=scanner.nextFloat();
double d=scanner.nextDouble();
char c=scanner.next().charAt(0);//next()读取字符串,charAt(0)获取字符串的第一个字符
String str1=scanner.next();//自动跳过开头的空白字符,到分隔符(空格、回车、Tab等)为止
String str2=scanner.nextLine();//包含空格,不包含回车符本身
scanner.close();

1.5.2 输出

字符串常量不能出现“回行”,下面写法无法编译:

System.out.println("你好,
               很高兴认识你 ");

需要加上“+”:

System.out.println("你好" +
                "再见");

out也可以使用printf()方法:System.out.printf("格式控制部分",表达式1,……,表达式n)

•%d:输出 int 型数据。
•%c:输出 char 型数据。
•%f:输出浮点型数据,小数部分最多保留6位。
• %s:输出字符串数据。
在输出数据时也可以控制数据在命令行中的位置。
•%md:输出的int 型数据占m列。
•%m.nf:输出的浮点型数据占m列,小数部分保留n位。

1.6 数组

1.6.1 数组的声明

两种格式:

 int []a;
 float boy[];//一维数组
 double b[][];
 char [][]car;//二维数组

声明多个数组:

int []a,b;//等价于int a[],b[];
int[]c,d[]; //一维数组c,二维数组d

1.6.2 为数组分配元素

数组名 = new 数组元素的类型 [ 数组元素的个数 ];

声明和创建数组可以分别完成或者一起完成:

float boy[];
boy=new float[2];

float boy2[]=new float[2];

构成二维数组的一位数组的长度可以不同:

 int a[][]=new int[2][];
 a[0]=new int[2];   
 a[1]=new int[4];

1.6.3 数组的哈希码

String[]arr={"zhhangsan","lisi"};
System.out.println(arr);//[Ljava.lang.String;@129a8472
  • [Ljava.lang.String;:表示这是一个 String 类型的数组([ 代表数组,L 代表引用类型,java.lang.String 是元素类型);
  • @129a8472:数组对象的哈希码(十六进制),用于标识对象在内存中的唯一性。

1.6.4 数组长度

数组名.length为数组中元素的个数,数组遍历的快速操作(idea提供):数组名.fori

int a[]=new int[3];//a.length为3
int b[][]=new int[5][6];//b.length为5

1.6.5 数组的初始化

声明的时候初始化:

int a[]={2,4,5,3};
int b[][]={{1},{2,2},{3,3,3}};

1.6.6 数组的引用

 int a[]={2,4,5,3},b[]={7,9};
 a=b;
 for (int n:a){
 System.out.println(n);
 }//7 9

对于普通类型数组,System.out.println(a);会输出a的引用;

对于char数组,System.out.println(a);会输出a的全部元素的值。

char a[]={'1','2','3','4'};
System.out.println(a);//1234

1.6.7 数组的内存图

Java内存分配
  • 栈:方法运行时使用的内存,比如main方法运行,进入方法栈中执行
  • 堆:存储对象或者数组,new来创建的,都存储在堆内存
  • 方法区:存储可以运行的class文件
  • 本地方法栈:JVM在使用操作系统功能的时候使用,和开发无关
  • 寄存器:给CPU使用,和开发无关

  1. 只要是new出来的一定是在堆里面开辟了一个小空间
  2. 如果new了多次,那么在堆里面有多个小空间,每个小空间都有各自的数据
  3. 当两个数组指向同一个小空间时,其中一个数组对小空间中的值发生了改变,那么其他数组再次访问的时候就是修改之后的结果了

1.7 字面量

Java 中常见的字面量类型:

  • 基本类型字面量:如整数 100、小数 3.14、字符 'a'、布尔值 false
  • 字符串字面量:用双引号包裹的字符序列,如 "Hello""Java"
  • null 字面量:仅 null,表示 “空引用”。

二、运算符、表达式和语句

2.1 运算符和表达式

运算符:算术运算符,自增自减运算符,关系运算符,逻辑运算符,赋值运算符,位运算符,instanceof运算符。

2.1.1算术混合运算的精度

从低到高:byte < short < char < int < long < float < double 

如果表达式中的最高精度低于int型整数,则按int精度进行运算,例如(byte)10+a结果为int型。

Java允许把不超出byte、short、char的取值范围的常量算术表达式的值赋给byte、short和char型变量。例如(byte)20+'a'为127正确,(byte)30+'b'编译错误,结果值超过了byte变量的取值范围。

byte x=97 正确,byte范围为-128~127;byte x=97+1 正确,在范围内,Java自动进行窄化转换;

byte y=1,byte z=97+y 不正确,变量参与运算时不会自动窄化,需要强制类型转化byte z=(byte)(97+y)。

‘苹’+‘果’的结果是int型数据;(short)10+‘a’的结果是int型数据。

2.1.2逻辑运算符

与&&或||非!

2.1.3位运算符

符号作用说明
&按位与都为1则为1
|按位或都为0则为0
 ~按位取反1则为0,0则为1
^按位异或相同则为0

a^a=0;a^0=a;若c=a^b,那么a=c^b;^的逆运算还是^,即a^b^b=a。

2.1.4 instanceof运算符

二目运算符,当左边的对象是右边的类或子类创建的对象时,运算结果为true,否则为false。

对象 instanceof 类/接口

用途:

1. 检查对象是否为某个类的实例

class Car {}  
public class Main {  
    public static void main(String[] args) {  
        Car car = new Car();  
        System.out.println(car instanceof Car); // true(car 是 Car 类的实例)  
    }  
}  

2. 检查对象是否为子类的实例

class Animal {}          // 父类  
class Dog extends Animal {} // 子类  
public class Main {  
    public static void main(String[] args) {  
        Dog dog = new Dog();  
        System.out.println(dog instanceof Animal); // true(Dog 是 Animal 的子类)  
    }  
}  

3. 检查对象是否实现某个接口

interface Flyable { void fly(); } // 接口  
class Bird implements Flyable {   // 实现接口的类  
    public void fly() { System.out.println("Bird flying"); }  
}  
public class Main {  
    public static void main(String[] args) {  
        Bird bird = new Bird();  
        System.out.println(bird instanceof Flyable); // true(Bird 实现了 Flyable)  
    }  
}  

2.2 语句

2.2.1 语句分类

方法调用语句、表达式语句、复合语句、空语句、控制语句、package语句和import语句。

2.2.2 if条件分支语句

if语句、if-else语句、if-else if- else语句

if 语句的条件表达式必须是 boolean 型,(如 if(1) 在 Java 中语法错误)。

2.2.3 switch开关语句

表达式和常量值可以是byte、short、int、char和String

可以同时没有break和default语句

2.2.4 循环语句

for循环(知道循环的次数或者循环的范围)、while循环(不知道循环的次数和范围,只知道循环的结束条件)、do-while语句

Java 中 while 循环的条件表达式必须是 boolean 型(如 while(1) 在 Java 中报错)。

2.2.5 break和continue语句

break整个循环结束,continue本次循环结束。

2.2.6 for语句与数组

三、类与对象

3.1 类

3.1.1 变量

成员变量

属于类或对象的长期属性,有默认值,可加访问修饰符(public/private 等)。

  • 实例变量:无static修饰,属于每个对象,对象间互不影响,创建对象时才分配内存,比如每个狗的name不同;
  • 类变量:加static修饰,属于类本身,所有对象共享,不用创建对象就能通过类名.变量名访问(如Dog.species),修改后所有对象都受影响。
局部变量

仅在局部范围有效,无默认值(必须手动赋值),不能加 public/private 等访问修饰符。

  • 普通局部变量:方法 /if/for 代码块内,作用域仅限当前代码块,用完即销毁。public void eat( ) { int count = 1; }
  • 参数变量:方法 / 构造方法参数,调用方法时传值,作用域仅限方法内。public Dog(String name) { this.name = name; }
常量

不是独立的 “变量类型”,而是给上述任意变量加final修饰,使其赋值后不能修改。

成员变量类型:整型、浮点型、字符型、逻辑类型、数组、对象、接口

3.1.2 方法

局部变量只在方法内有效,与声明的位置有关。方法的参数在整个方法内有效,方法内的局部变量从声明它的位置之后开始有效。

如果局部变量的名字和成员变量的名字相同,成员变量被隐藏。如果要使用被隐藏的成员变量,必须使用this关键字。

成员变量有默认值,局部变量没有默认值。

class InitError{
    int x=10,y;//成员变量y默认值为0
    void f(){
        int m;//局部变量没有默认值
        x=y+m;//无法通过编译
    }
}

方法的定义在main方法的外面,调用在main方法的里面(先定义后调用)。

类体包含两部分:变量的声明(声明时可赋初值)和方法的定义。对成员变量的操作只能放在方法里面。

class A{
    int x,y=10;//声明的时候赋初值
    x=10;//非法
    }

3.2 构造方法与对象的创建

3.2.1 构造方法

构造方法的名字必须与所在类的名字相同,而且没有类型。

当一个类的实例对象刚产生(即通过new关键字创建对象)时,构造方法会被自动调用。

Ladder(){
}

如果没有编写构造方法,系统默认只有一个构造方法,无参数,方法体中没有语句;如果定义了一个或多个构造方法,系统不会提供默认构造方法,但要保证参数不同(个数,类型)。

class Point{
    int x,y;
    Point(){
        x=y=1;
    }
    void Point(int a,int b){//不是构造方法
        x=a;
        y=b;
    }
    int Point() {//不是构造方法
        return 21;
    }
}

3.2.2 创建方法

对象的声明——>为声明的对象分配变量 

class Point{
    int x,y;
    Point(int a,int b){//不是构造方法
        x=a;
        y=b;
    }
}

public class test01 {
    public static void main(String[] args) {
        Point p1,p2;//声明对象
        p1=new Point(1,2);//为对象分配变量
        p2=new Point();//非法
            }
        }

3.2.3 对象的引用和实体

通过使用访问符“·”对象可以实现对自己的变量的访问和方法的调用。

3.3 参数的传值

3.3.1 可变参数

可变参数 是一种允许方法接收可变数量参数的语法特性。

格式为:参数类型... 参数名;

public void printNumbers(int... nums) {
    // 方法体
}
printNumbers(); // 传0个参数
printNumbers(1, 2, 3); // 传多个参数
int[] arr = {4, 5, 6};
printNumbers(arr); // 传数组

必须是方法的最后一个参数:一个方法中最多只能有一个可变参数,且必须放在参数列表的末尾。

// 正确:可变参数在最后
public void demo(String str, int... nums) {}

// 错误:可变参数不是最后一个
public void demo(int... nums, String str) {} 

3.3.2 按值传递和按引用传递

传值机制:方法的参数为形参,传值的变量或常量为实参。相互改变不会影响对方。

按值传递:向该参数传递的值不能高于该参数的级别。比如不能向int型参数传递一个float值。

按引用传递:传递的是对象引用的 “值”(即引用本身的拷贝,而非对象本身)。此时,方法内的引用和外部的引用指向同一个对象,但引用本身是独立的拷贝。

场景 1:修改 “引用指向的对象的属性”

class Person {
    String name;
    public Person(String name) {
        this.name = name;
    }
}

public class Demo {
    public static void changeName(Person p) {
        p.name = "修改后的名字"; // 修改对象的属性
    }

    public static void main(String[] args) {
        Person person = new Person("原名字");
        changeName(person);
        System.out.println(person.name); // 输出“修改后的名字”
    }
}

场景 2:修改 “引用本身(让引用指向新对象)”

class Person {
    String name;
    public Person(String name) {
        this.name = name;
    }
}

public class Demo {
    public static void changeRef(Person p) {
        p = new Person("新对象"); // 让形参指向新对象
    }

    public static void main(String[] args) {
        Person person = new Person("原对象");
        changeRef(person);
        System.out.println(person.name); // 输出“原对象”(形参的新指向不影响实参)
    }
}

Java 中只有 “值传递”

  • 基本类型传递 “值的拷贝”,方法内修改不影响外部。
  • 引用类型传递 “引用的拷贝”:
    • 若修改对象的属性,会影响外部(因为共享同一个对象)。
    • 若修改引用本身的指向,不会影响外部(因为只是修改了拷贝的引用)。

3.4 对象的组合

一个类将另一个类的对象作为自身的成员变量,从而使该类具备 “组合对象” 的功能或属性。

3.4.1 组合和复用

在类 A 中,将类 B 的对象作为成员变量(即 “组件对象”),使得类 A(“组合类”)可以通过操作这个组件对象,间接拥有类 B 的行为或属性。

// 组件类1:引擎
class Engine {
    public void start() {
        System.out.println("引擎启动");
    }
}

// 组件类2:轮胎
class Tire {
    public void rotate() {
        System.out.println("轮胎旋转");
    }
}

// 组合类:汽车(包含引擎和轮胎对象)
class Car {
    // 声明组件对象为成员变量
    private Engine engine;
    private Tire tire;

    // 构造方法中初始化组件对象
    public Car() {
        this.engine = new Engine();
        this.tire = new Tire();
    }

    // 汽车的“行驶”行为,依赖组件对象的方法
    public void drive() {
        engine.start();   // 调用引擎的启动方法
        tire.rotate();    // 调用轮胎的旋转方法
        System.out.println("汽车开始行驶");
    }
}

// 测试类
public class Test {
    public static void main(String[] args) {
        Car car = new Car();
        car.drive();  // 输出:引擎启动 → 轮胎旋转 → 汽车开始行驶
    }
}

典型场景

  • GUI 开发:窗口(Window)组合 “按钮(Button)”“文本框(TextField)” 等组件。
  • 业务系统:订单服务(OrderService)组合 “用户 DAO”“商品 DAO” 等对象,完成下单逻辑。
  • 工具类设计:如ArrayList内部组合了Object[]数组来存储元素。

3.5 实例成员和类成员

3.5.1 实例变量和类变量

class Student {
    // 实例变量:每个学生对象的“特有属性”
    private String name; // 姓名,每个学生不同
    private int age;     // 年龄,每个学生不同
    
    // 类变量:所有学生的“共有属性”
    public static int totalNum = 0; // 学生总数量,所有学生共享
    
    // 构造方法:创建对象时初始化实例变量,并更新类变量
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
        totalNum++; // 每创建一个学生,总数量+1
    }
    
    // Getter方法(略)...
}

public class Test {
    public static void main(String[] args) {
        // 创建两个学生对象
        Student s1 = new Student("张三", 18);
        Student s2 = new Student("李四", 19);
        
        // 访问实例变量:通过对象
        System.out.println(s1.name); // 输出“张三”
        System.out.println(s2.age);  // 输出19
        
        // 访问类变量:通过类名(推荐)或对象
        System.out.println(Student.totalNum); // 输出2
        System.out.println(s1.totalNum);      // 输出2(不推荐,易误解为对象属性)
    }
}

3.5.2 实例方法和类方法

public class Person {
    // 实例变量(属于对象)
    private String name;
    // 类变量(属于类)
    private static String species = "人类";

    // 实例方法(无 static)
    public void showName() {
        // 访问实例变量 name(this 可省略)
        System.out.println("姓名:" + this.name);
        // 访问类变量 species
        System.out.println("物种:" + species);
    }

    // 实例方法调用类方法
    public void showSpecies() {
        printSpecies(); // 直接调用类方法
    }

    // 类方法(有 static)
    public static void printSpecies() {
        System.out.println("物种:" + species);
    }

    public static void main(String[] args) {
        // 1. 调用实例方法:必须先创建对象
        Person person = new Person();
        person.name = "张三";
        person.showName(); // 输出:姓名:张三 / 物种:人类
        person.showSpecies(); // 输出:物种:人类

        // 2. 错误:不能用类名直接调用实例方法
        // Person.showName(); // 编译报错
    }
}
public class MathUtil {
    // 类变量(静态变量)
    private static final double PI = 3.14159;

    // 类方法(static 修饰):计算圆的面积
    public static double calculateCircleArea(double radius) {
        // 访问类变量 PI(正确)
        return PI * radius * radius;

        // 错误:不能访问实例变量(若存在实例变量 x,此处编译报错)
        // return x * radius;
    }

    // 类方法调用其他类方法
    public static double calculateCircleCircumference(double radius) {
        return 2 * PI * radius;
    }

    public static void main(String[] args) {
        // 1. 推荐:用类名直接调用类方法
        double area = MathUtil.calculateCircleArea(2);
        System.out.println("圆面积:" + area); // 输出:12.56636

        // 2. 不推荐:通过对象调用类方法(语法允许,但逻辑上不合理)
        MathUtil util = new MathUtil();
        double circumference = util.calculateCircleCircumference(2);
        System.out.println("圆周长:" + circumference); // 输出:12.56636

        // 3. 错误:类方法中访问实例方法(编译报错)
        // util.someInstanceMethod(); // 若 someInstanceMethod 是实例方法,此处报错
    }
}

3.6 方法重载

同一个类中,定义多个方法名相同但参数列表不同(个数不同类型不同顺序不同(仅顺序不同时,必须保证类型不同的方法。

public class Calculator {
    // 1. 两个 int 类型求和(参数个数=2,类型=int)
    public int add(int a, int b) {
        System.out.println("int + int");
        return a + b;
    }

    // 2. 三个 int 类型求和(参数个数=3,与1构成重载)
    public int add(int a, int b, int c) {
        System.out.println("int + int + int");
        return a + b + c;
    }

    // 3. 两个 double 类型求和(参数类型=double,与1构成重载)
    public double add(double a, double b) {
        System.out.println("double + double");
        return a + b;
    }

    // 4. int + double 求和(参数顺序/类型不同,与1、3构成重载)
    public double add(int a, double b) {
        System.out.println("int + double");
        return a + b;
    }

    // 5. double + int 求和(参数顺序不同,与4构成重载)
    public double add(double a, int b) {
        System.out.println("double + int");
        return a + b;
    }

    // 错误示例:仅返回值不同,不算重载(编译报错:方法已存在)
    // public double add(int a, int b) {
    //     return a + b;
    // }

    public static void main(String[] args) {
        Calculator calc = new Calculator();
        System.out.println(calc.add(1, 2));          // 调用1:输出 int + int,结果3
        System.out.println(calc.add(1, 2, 3));       // 调用2:输出 int + int + int,结果6
        System.out.println(calc.add(1.5, 2.5));      // 调用3:输出 double + double,结果4.0
        System.out.println(calc.add(1, 2.5));        // 调用4:输出 int + double,结果3.5
        System.out.println(calc.add(1.5, 2));        // 调用5:输出 double + int,结果3.5
    }
}

3.7 this关键字

this关键字是一个当前对象的引用,它指向「正在调用当前方法 / 构造器的实例对象」。其核心作用是明确区分当前对象的成员(变量 / 方法)与局部变量 / 参数。

this 的 核心用法: 区分「成员变量」和「局部变量 / 参数」

方法参数 / 局部变量与类的成员变量同名时,会发生「变量隐藏」(局部变量覆盖成员变量),此时用 this.成员变量 明确指定访问当前对象的成员变量。

public class Person {
    // 成员变量(属于对象)
    private String name;
    private int age;

    // 构造器:参数名与成员变量名相同,用 this 区分
    public Person(String name, int age) {
        this.name = name; // this.name → 成员变量;name → 构造器参数
        this.age = age;   // this.age → 成员变量;age → 构造器参数
    }

    // setter 方法:参数名与成员变量名相同
    public void setName(String name) {
        this.name = name; // 避免局部变量覆盖成员变量
    }

    // 实例方法:局部变量与成员变量同名
    public void showInfo(String name) {
        System.out.println("局部变量 name:" + name);
        System.out.println("成员变量 name:" + this.name); // 明确访问成员变量
        System.out.println("成员变量 age:" + this.age);
    }

    public static void main(String[] args) {
        Person person = new Person("张三", 20);
        person.showInfo("李四"); 
        // 输出:
        // 局部变量 name:李四
        // 成员变量 name:张三
        // 成员变量 age:20
    }
}

3.8 package 包

包是 Java 中组织类、接口的核心机制,主要作用是避免命名冲突、管理代码结构、控制访问权限

3.8.1. 定义

  • 使用package关键字声明包,语句必须放在源文件的第一行(注释除外)
  • 语法:package 包名层级;(层级用.分隔,对应文件系统的目录结构)。示例:package com.example.utils;(对应文件目录com/example/utils/)。

3.8.2. 导入

通过import语句引入其他包的类 / 接口,避免每次写全限定类名:

  • 导入单个类:import java.util.ArrayList;
  • 导入包下所有类(不包含子包):import java.util.*;
  • 静态导入(直接使用类的静态成员):import static java.lang.Math.PI;(可直接用PI,无需Math.PI)。

3.8.3. 包的访问权限

包可以配合访问修饰符,控制类 / 成员的可见范围:

  • 默认权限(无修饰符):仅当前包内的类可访问。
  • public:所有包的类均可访问。
  • protected:当前包内的类 + 子类(无论是否同包)可访问。
  • private:仅当前类可访问。

3.8.4. 常用标准包

Java 内置了大量工具包,常用的有:

  • java.lang:核心类(自动导入,无需import),包含StringSystemObject等。
  • java.util:工具类,包含集合(ArrayListHashMap)、日期(LocalDate)等。
  • java.io:输入输出类,如FileInputStream
  • java.net:网络编程类,如URLSocket
  • java.sql:数据库操作类,如ConnectionResultSet

3.8.5. 包的命名规范

通常采用反转的域名作为前缀(小写字母),避免命名冲突,示例:

  • 企业项目:com.aliyun.cloud
  • 个人项目:me.username.demo

3.9 import 语句

import是 Java 中用于引入其他包的类 / 接口的语句,核心作用是简化代码编写,避免重复书写类的全限定名(包名 + 类名)。

当需要使用其他包中的类 / 接口时,通过import引入后,可直接用类名调用,无需每次写 “包名。类名” 的完整形式。

3.9.1 常见用法

(1)导入单个类 / 接口

语法:import 包名.类名;示例:

import java.util.ArrayList; // 导入java.util包下的ArrayList类
ArrayList<String> list = new ArrayList<>(); // 直接使用类名
(2)导入包下的所有类(当前包,不含子包)
import java.util.*; // 导入java.util包下的所有类(如ArrayList、HashMap等)
HashMap<String, Integer> map = new HashMap<>();
(3)静态导入(导入类的静态成员)

用于直接引入类的静态变量 / 静态方法,无需通过 “类名。静态成员” 调用。语法:

  • 导入单个静态成员:import static 包名.类名.静态成员;
  • 导入类的所有静态成员:import static 包名.类名.*;
import static java.lang.Math.PI; // 导入Math类的静态变量PI
import static java.lang.System.out; // 导入System类的静态方法out

System.out.println(PI); // 可简化为 out.println(PI);

3.9.2 注意事项

  • 自动导入的包java.lang包(包含StringSystemObject等核心类)会被 JVM 自动导入,无需手动写import
  • 子包不被通配符覆盖:比如import java.util.*;不会导入java.util.concurrent子包的类,若需使用子包类,需单独导入(如import java.util.concurrent.ThreadPoolExecutor;)。
  • 同名类冲突:若导入的两个包包含同名类(如java.util.Datejava.sql.Date),需用全限定名区分,或避免通配符导入。

3.10 访问权限

各修饰符的适用场景

private

仅当前类内部可访问,用于封装类的 “内部细节”(如私有成员变量、工具方法),避免外部直接修改或调用。

示例:类的私有成员变量,需通过get/set方法对外暴露。

默认权限(无修饰符)

仅当前包内的类可访问,适合包内的 “工具类 / 辅助类”,避免其他包无关代码调用。

示例:包内通用的工具方法(如com.example.utils包内的辅助函数)。

protected

同包类 + 不同包的子类可访问,用于 “继承场景”—— 既保证包内复用,又允许子类扩展。

示例:父类的成员方法,希望子类能重写,但不对外暴露。

public

所有包的类均可访问,用于对外提供的 “公共接口”(如工具类的核心方法、业务类的对外功能)。

示例:java.util.ArrayList类(public 修饰,全局可使用)。

3.11 基本类型的类封装

3.12 var局部变量

3.13 对象数组

3.14 jar文件

3.15 方法的基本内存原理

  • 基本数据类型:数据值是存储在自己的空间(栈)中。赋值给其他变量,也是赋的真实的值。
  • 引用数据类型:数据值是存储在其他空间(堆)中,自己空间(栈)中存储的是地址值。赋值给其他变量,赋的地址值。
public static void main(String[] args) {
    int num=100;
    System.out.println(num);//100
    change(num);
    System.out.println(num);//100
}
public static void change(int num) {
     num=200;
}

public static void main(String[] args) {
    int num=100;
    System.out.println(num);//100
    num =change(num);
    System.out.println(num);//200
}
public static int change(int num) {
    num=200;
    return num;
}

对比以上代码,传递基本数据类型时,传递的是真实的数据,形参的改变不影响实际参数的值。

 public static void main(String[] args) {
        int []arr={1,2,3};
        System.out.println(arr[1]);//2
        change(arr);
        System.out.println(arr[1]);//20
    }
    public static void change(int []arr) {
        arr[1]=20;
    }

传递引用数据类型时,传递的是地址值,形参的改变影响实际参数的值

四、子类与继承

4.1 子类与父类

  • 父类(超类 / 基类):定义了一组通用的属性(变量)和行为(方法),是 “通用模板”。
  • 子类(派生类):通过 extends 关键字继承父类,完全复用父类的非私有属性 / 方法,还能新增自己特有的属性 / 方法,或修改父类的方法(重写)。

关键规则

  1. 单继承限制:Java 中一个子类只能直接继承一个父类(比如 Dog 不能同时继承 Animal 和 Cat),但可以多层继承(比如「泰迪」继承「狗」,「狗」继承「动物」)。
  2. 私有成员不继承:父类中用 private 修饰的属性 / 方法(比如 private String color;),子类无法直接访问,需通过父类的 public/protected 方法间接访问。
  3. “is-a” 关系:子类和父类必须满足「是一种」的逻辑(比如 “狗是一种动物”、“学生是一种人”),不要为了复用代码强行继承。
  4. 构造方法不继承:父类的构造方法子类不能继承,但子类构造方法默认会调用父类的无参构造(可以用 super() 手动调用父类构造)。
// 1. 父类:动物(通用模板)
class Animal {
    // 父类的属性(非私有,子类能继承)
    String name;
    int age;

    // 父类的方法(通用行为)
    public void eat() {
        System.out.println(name + "在吃东西");
    }

    // 私有方法:子类不能继承(哪怕写了,子类也用不了)
    private void sleep() {
        System.out.println("睡觉");
    }
}

// 2. 子类:狗(继承Animal父类)
class Dog extends Animal {
    // 子类新增的特有属性
    String breed; // 品种

    // 子类新增的特有方法
    public void bark() {
        System.out.println(name + "汪汪叫");
    }

    // 子类重写父类的方法(修改父类的行为)
    @Override
    public void eat() {
        System.out.println(name + "在啃骨头"); // 替代父类的“吃东西”
    }
}

// 测试代码
public class Test {
    public static void main(String[] args) {
        Dog dog = new Dog();
        // 继承父类的属性
        dog.name = "旺财";
        // 继承父类但被重写的方法
        dog.eat(); // 输出:旺财在啃骨头
        // 子类特有方法
        dog.bark(); // 输出:旺财汪汪叫
     // dog.sleep(); // 报错!私有方法子类不能继承/调用
    }
}

注:@Override

 @Override 是一个注解(Annotation) —— 简单理解就是 “给编译器看的标记 / 提示”,核心作用是:告诉编译器「我这个方法是特意重写父类的方法,请你帮我检查写得对不对」

两个关键作用:

作用 1:语法检查,避免手误写错

作用 2:提升代码可读性

4.2 子类的继承性

子类和父类「同包」或「不同包」的继承性差异,核心只和父类成员的「访问修饰符」有关—— 继承的本质没变(还是复用父类非私有成员),但不同修饰符决定了子类在跨包 / 同包下能不能 “看到并使用” 父类的成员。

两个基础前提:

  1. private 修饰的成员:不管同包还是不同包,子类都不能继承 / 访问(父类的 “私有财产”,子类碰不到);
  2. 继承性的差异,只体现在 publicprotected、「默认修饰符」(没写任何修饰符,也叫包访问权限)这三类成员上。
// 包:com.test1(父类所在包)
package com.test1;

// 父类必须用public(否则不同包访问不到父类本身)
public class Animal {
    public String pubName;    // public修饰
    protected int proAge;     // protected修饰
    String defColor;          // 默认修饰
    private double priWeight; // private修饰

    public void pubEat() { System.out.println("public吃东西"); }
    protected void proRun() { System.out.println("protected跑"); }
    void defSleep() { System.out.println("默认睡觉"); }
    private void priDrink() { System.out.println("private喝水"); }
}

// 包:com.test2(子类所在包)
package com.test2;
// 导入父类
import com.test1.Animal;

// 子类:Dog(和Animal不同包)
public class Dog extends Animal {
    public void useParentMember() {
        // 能继承/访问:public、protected的属性和方法
        pubName = "旺财";
        proAge = 2;
        pubEat();
        proRun();

        // 不能访问:默认、private的成员(编译报错)
        // defColor = "黄色"; 
        // defSleep();
        // priWeight = 10.0; 
        // priDrink();
    }
}

关键总结

  1. 跨包继承的前提:父类必须用 public 修饰(否则不同包连父类都找不到,谈何继承);
  2. 核心差异:同包能继承默认修饰的成员,跨包只能继承 public/protected
  3. protected 是 “跨包子类专属”:专门解决 “不同包的子类想继承父类成员” 的需求;
  4. private 永远隔离:不管在哪,子类都碰不到父类的 private 成员。

4.3 子类与对象

子类对象的核心特征

特征 1:子类对象能直接用父类的非私有成员

// 父类:动物(通用模板)
class Animal {
    String name; // 父类的非私有属性
    public void eat() { // 父类的非私有方法
        System.out.println(name + "吃东西");
    }
}

// 子类:狗(继承Animal)
class Dog extends Animal {
    String breed; // 子类特有属性
    public void bark() { // 子类特有方法
        System.out.println(name + "汪汪叫");
    }
}

// 测试:创建子类对象,用父类+子类的成员
public class Test {
    public static void main(String[] args) {
        // 1. 创建子类对象(旺财:狗子类的具体实例)
        Dog wangcai = new Dog();
        
        // 2. 用父类的成员(继承来的)
        wangcai.name = "旺财"; // 父类的属性
        wangcai.eat(); // 父类的方法 → 输出:旺财吃东西
        
        // 3. 用子类的特有成员
        wangcai.breed = "金毛"; // 子类的属性
        wangcai.bark(); // 子类的方法 → 输出:旺财汪汪叫
    }
}

特征 2:子类对象创建时,先初始化父类部分

class Animal {
    // 父类构造方法
    public Animal() {
        System.out.println("父类Animal的构造方法执行了");
    }
}

class Dog extends Animal {
    // 子类构造方法
    public Dog() {
        // 这里默认隐藏了一句:super(); → 调用父类无参构造
        System.out.println("子类Dog的构造方法执行了");
    }
}

// 测试:创建子类对象,看构造顺序
public class Test {
    public static void main(String[] args) {
        Dog wangcai = new Dog();
        // 输出顺序:
        // 1. 父类Animal的构造方法执行了
        // 2. 子类Dog的构造方法执行了
    }
}

特征 3:父类引用可以指向子类对象( “向上转型”)

这是继承的核心灵活点:可以用父类的 “类型” 接收子类对象,这个对象依然是子类的实例(能调用父类方法,或子类重写的方法):

public class Test {
    public static void main(String[] args) {
        // 父类类型(Animal)指向子类对象(Dog)
        Animal animal = new Dog();
        animal.name = "旺财";
        animal.eat(); // 若Dog重写了eat(),会执行子类的eat()
        
        // 注意:父类引用不能直接调用子类特有方法(比如bark())
        // animal.bark(); // 报错!因为Animal类型“看不到”子类的bark()
    }
}

特点 4:重写的方法优先执行子类版本

class Animal {
    public void eat() { System.out.println("动物吃东西"); }
}
class Dog extends Animal {
    @Override
    public void eat() { System.out.println("狗啃骨头"); } // 重写父类方法
}

public class Test {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.eat(); // 输出“狗啃骨头”(子类重写的版本)
    }
}

总结:

子类是 “包含父类特征的模板”,子类对象是这个模板的具体实例 —— 它既有父类的通用能力,又有自己的专属能力,创建时会先把父类的部分初始化好。

instanceof 运算符

instanceof 是 Java 的关键字,核心作用:判断一个对象是否是某个类(或其父类 / 接口)的实例,返回 true 或 false

核心用途:

  • 场景 1:判断父类引用指向的对象,到底是哪个子类的实例;
  • 场景 2:向下转型(把子类对象从父类引用转回子类类型)前做判断,避免报错。
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

public class Test {
    public static void main(String[] args) {
        // 1. 子类对象判断自身类型
        Dog wangcai = new Dog();
        System.out.println(wangcai instanceof Dog); // true(是Dog的实例)
        System.out.println(wangcai instanceof Animal); // true(是父类Animal的实例)
        System.out.println(wangcai instanceof Cat); // false(和Cat无关)

        // 2. 父类引用指向子类对象,判断类型(核心场景)
        Animal animal = new Dog();
        if (animal instanceof Dog) {
            // 向下转型:把Animal类型转回Dog类型,才能调用子类特有方法
            Dog dog = (Dog) animal;
            dog.bark(); // 转型后就能调用bark()了
        }
        if (animal instanceof Cat) { // 条件为false,不会执行
            Cat cat = (Cat) animal;
        }
    }
}

4.4 成员变量的隐藏和方法重写

核心:变量只看 “引用是谁”,方法只看 “对象是谁”

成员变量的隐藏(看引用类型)

子类中定义了和父类同名的非私有成员变量,就会 “隐藏” 父类的这个变量 —— 注意:不是替换,而是父类的变量依然存在,只是子类默认访问自己的同名变量,父类变量被 “藏” 起来了。

// 父类:Animal
class Animal {
    String name = "动物的名字"; // 父类的成员变量
}

// 子类:Dog(定义和父类同名的name变量,隐藏父类的name)
class Dog extends Animal {
    String name = "狗的名字"; // 子类的同名变量,隐藏父类的name
}

public class Test {
    public static void main(String[] args) {
        // 场景1:子类引用指向子类对象 → 访问子类自己的name
        Dog dog = new Dog();
        System.out.println(dog.name); // 输出:狗的名字

        // 场景2:父类引用指向子类对象 → 访问父类的name(核心!看引用类型)
        Animal animal = new Dog();
        System.out.println(animal.name); // 输出:动物的名字
    }
}

方法重写(看对象实际类型)

子类写一个和父类方法名、参数列表、返回值(兼容)完全一致的方法,就会 “重写” 父类的这个方法 —— 此时父类的方法被覆盖,调用时优先执行子类的方法。

// 父类:Animal
class Animal {
    public void eat() { // 父类的eat方法
        System.out.println("动物吃东西");
    }
}

// 子类:Dog(重写父类的eat方法)
class Dog extends Animal {
    @Override // 注解检查重写是否正确
    public void eat() { // 方法名、参数和父类完全一致
        System.out.println("狗啃骨头");
    }
}

public class Test {
    public static void main(String[] args) {
        // 场景1:子类引用指向子类对象 → 执行子类的eat
        Dog dog = new Dog();
        dog.eat(); // 输出:狗啃骨头

        // 场景2:父类引用指向子类对象 → 还是执行子类的eat(核心!看对象实际类型)
        Animal animal = new Dog();
        animal.eat(); // 输出:狗啃骨头
    }
}
  • 必须满足 “三一致”:方法名一致、参数列表一致(个数 / 类型 / 顺序)、返回值兼容;

4.5 super 关键字

super 翻译过来是 “超级、父类”,核心作用是:在子类中明确访问父类的成员(属性、方法、构造方法),解决因 “变量隐藏” 或 “方法重写” 导致父类成员访问不到的问题。

用法 1:super. 属性 → 访问父类被隐藏的成员变量

class Animal {
    String name = "动物的名字"; // 父类变量
}

class Dog extends Animal {
    String name = "狗的名字"; // 子类同名变量(隐藏父类)
    
    public void showName() {
        System.out.println("子类name:" + name); // 自己的name → 狗的名字
        System.out.println("父类name:" + super.name); // 父类的name → 动物的名字
    }
}

public class Test {
    public static void main(String[] args) {
        new Dog().showName();
    }
}

用法2:super. 方法名 () → 访问父类被重写的方法

class Animal {
    public void eat() {
        System.out.println("动物吃东西");
    }
}

class Dog extends Animal {
    @Override
    public void eat() {
        super.eat(); // 先调用父类的eat() → 动物吃东西
        System.out.println("狗啃骨头"); // 再执行自己的逻辑
    }
}

public class Test {
    public static void main(String[] args) {
        new Dog().eat(); 
        // 输出:
        // 动物吃东西
        // 狗啃骨头
    }
}

用法3:super () → 调用父类的构造方法

子类创建对象时,默认会隐式调用父类无参构造(相当于第一行有 super());如果想调用父类有参构造,必须手动写 super(参数),且必须放在子类构造方法的第一行

class Animal {
    // 父类有参构造
    public Animal(String name) {
        System.out.println("父类构造:" + name);
    }
    
    // 父类无参构造(如果不写,子类默认调用会报错)
    public Animal() {}
}

class Dog extends Animal {
    public Dog() {
        super("动物"); // 手动调用父类有参构造,必须在第一行
        System.out.println("子类Dog构造");
    }
}

public class Test {
    public static void main(String[] args) {
        new Dog();
        // 输出:
        // 父类构造:动物
        // 子类Dog构造
    }
}
  • super 只能用在子类中;
  • super() 只能在子类构造方法第一行,且不能和 this()(子类自己的构造)同时用;
  • super 指向的是当前子类对象中的 “父类部分”。

4.6 final 关键字

final 翻译是 “最终的、不可修改的”,核心作用是给元素加 “限制”,让它不能被改变。

用法 1:final 修饰父类 → 父类不能被继承

// final修饰Animal,这个类不能被继承
final class Animal {
    public void eat() {}
}

// 报错!Dog不能继承final的Animal
// class Dog extends Animal {}

用法 2:final 修饰父类方法 → 父类方法不能被子类重写

class Animal {
    // final修饰eat方法,子类不能重写
    public final void eat() {
        System.out.println("动物吃东西");
    }
}

class Dog extends Animal {
    // 报错!不能重写final的eat方法
    // @Override
    // public void eat() {}
}

用法 3:final 修饰变量 → 变量变成常量,赋值后不能改

final 修饰的变量(成员变量 / 局部变量),只能赋值一次,后续不能修改:

class Dog {
    // final成员变量:要么声明时赋值,要么在构造方法中赋值
    final String breed = "金毛"; 
    
    public void changeBreed() {
        // 报错!final变量不能重新赋值
        // breed = "哈士奇";
    }
}

public class Test {
    public static void main(String[] args) {
        final int age = 2;
        // 报错!局部final变量也不能改
        // age = 3;
    }
}
  • final 修饰基本类型变量:值不能改;
  • final 修饰引用类型变量(比如对象):引用不能改(不能指向新对象),但对象内部的属性可以改;
  • final 方法只是不能重写,子类依然可以正常调用。

4.7 对象的上转型对象

上转型对象 = 父类类型的引用变量 指向 子类类型的实际对象(比如 Animal animal = new Dog();

// 父类:Animal
class Animal {
    String name = "动物"; // 父类变量
    public void eat() { // 父类方法
        System.out.println("动物吃东西");
    }
}

// 子类:Dog
class Dog extends Animal {
    String name = "狗"; // 子类同名变量(隐藏父类)
    @Override
    public void eat() { // 重写父类方法
        System.out.println("狗啃骨头");
    }
    public void bark() { // 子类特有方法(父类没有)
        System.out.println("汪汪叫");
    }
}

public class Test {
    public static void main(String[] args) {
        // 👇 创建上转型对象:父类引用指向子类对象
        Animal animal = new Dog(); 
        // animal就是Dog对象的“上转型对象”
    }
}
特征 1:调用方法→看 “实际对象(子类)”,执行重写后的逻辑

animal.eat(); // 输出:狗啃骨头(不是父类的“动物吃东西”)

特征 2:访问变量→看 “引用类型(父类)”,取父类的变量

System.out.println(animal.name); // 输出:动物(不是子类的“狗”)

特征 3:不能直接调用子类的特有方法

// animal.bark(); // 报错!Animal类型没有bark()方法

特征 4:可转回子类类型(向下转型),恢复全部能力
if (animal instanceof Dog) { // 先判断是不是Dog的实例
    Dog dog = (Dog) animal; // 向下转型
    dog.bark(); // 输出:汪汪叫(恢复子类特有能力)
}

核心用途

上转型对象的最大价值是统一管理不同子类对象,体现 “多态”—— 比如写一个方法接收父类类型,就能传入任意子类对象,不用为每个子类写重复方法:

// 统一的“喂食”方法:接收Animal类型(上转型对象)
public static void feed(Animal animal) {
    animal.eat();
}

public static void main(String[] args) {
    // 传入Dog的上转型对象
    feed(new Dog()); // 输出:狗啃骨头
    
    // 如果有Cat子类,也能直接传(无需改feed方法)
    // feed(new Cat()); // 输出:猫吃鱼(假设Cat重写了eat())
}
  1. 上转型对象不是 “新对象”:只是给已有子类对象换了个父类引用,对象本身没变;
  2. 核心规则:方法看对象(子类),变量看引用(父类)
  3. 向下转型要谨慎:必须用instanceof判断类型,否则可能报ClassCastException(类型转换异常)。

4.8 继承与多态

继承是多态的基础(没有继承就没有这种多态),多态是继承的 “灵活应用”

  • 继承:子类继承父类的通用特征(属性 / 方法),是 “基础模板”(比如狗、猫都继承 “动物” 的 eat () 方法);
  • 多态:基于继承,同一个父类引用指向不同子类对象时,调用同一个方法,会执行不同子类的逻辑(比如动物的 eat (),狗执行 “啃骨头”,猫执行 “吃鱼”)。
// 1. 继承:定义父类(通用模板)
class Animal {
    // 父类的通用方法(多态的核心:子类要重写这个方法)
    public void eat() {
        System.out.println("动物吃东西");
    }
}

// 2. 继承:子类Dog继承Animal,重写eat()
class Dog extends Animal {
    @Override
    public void eat() { // 重写父类方法,体现子类的特有行为
        System.out.println("狗啃骨头");
    }
}

// 2. 继承:子类Cat继承Animal,重写eat()
class Cat extends Animal {
    @Override
    public void eat() { // 重写父类方法,体现子类的特有行为
        System.out.println("猫吃鱼");
    }
}

// 3. 多态:同一个父类引用,适配不同子类对象
public class Test {
    // 统一的方法:接收父类类型(上转型对象)
    public static void feed(Animal animal) {
        animal.eat(); // 调用eat(),但执行结果由实际子类对象决定
    }

    public static void main(String[] args) {
        // 父类引用指向Dog对象(上转型对象)
        Animal dog = new Dog();
        feed(dog); // 输出:狗啃骨头
        
        // 父类引用指向Cat对象(上转型对象)
        Animal cat = new Cat();
        feed(cat); // 输出:猫吃鱼
        
        // 甚至可以直接传子类对象,自动向上转型
        feed(new Dog()); // 输出:狗啃骨头
        feed(new Cat()); // 输出:猫吃鱼
    }
}

多态必须满足 3 个条件:

  1. 有继承:子类必须继承父类(比如 Dog/Cat 继承 Animal);
  2. 有方法重写:子类必须重写父类的方法(比如 Dog/Cat 重写 eat ());
  3. 有上转型对象:父类引用指向子类对象(比如Animal animal = new Dog();)。

易混点:

  • 多态只针对方法:调用方法时看 “实际子类对象”;
  • 多态不针对变量:访问变量时还是看 “父类引用”(比如animal.name取父类的 name);
  • 没有继承就没有这种多态:比如两个无继承关系的类,无法用父类引用统一管理。

4.9 abstract类和方法

抽象类是 “不完整的父类模板”,抽象方法是 “只定规则、不写具体逻辑的方法”

关键前提:有抽象方法的类,必须是抽象类

// 1. 抽象类:Animal(不能直接new,只能被继承)
abstract class Animal {
    String name; // 抽象类可以有普通成员变量
    
    // 普通方法:抽象类也可以有完整的方法
    public void breathe() {
        System.out.println(name + "呼吸空气");
    }
    
    // 2. 抽象方法:只有签名,没有方法体(必须以;结尾)
    abstract public void eat(); // 规则:所有动物都要“吃”,但具体怎么吃,子类自己定
}

// 3. 子类Dog继承抽象类,必须重写所有抽象方法(否则编译报错)
class Dog extends Animal {
    public Dog(String name) {
        this.name = name;
    }
    
    // 重写抽象方法:补全“吃”的具体逻辑
    @Override
    public void eat() {
        System.out.println(name + "啃骨头");
    }
}

// 子类Cat同理
class Cat extends Animal {
    public Cat(String name) {
        this.name = name;
    }
    
    @Override
    public void eat() {
        System.out.println(name + "吃鱼");
    }
}

// 测试代码
public class Test {
    public static void main(String[] args) {
        // 错误!抽象类不能直接创建对象
        // Animal animal = new Animal(); 
        
        // 正确:创建子类对象(抽象类的唯一使用方式)
        Animal dog = new Dog("旺财");
        dog.eat(); // 输出:旺财啃骨头
        dog.breathe(); // 输出:旺财呼吸空气
        
        Animal cat = new Cat("咪咪");
        cat.eat(); // 输出:咪咪吃鱼
    }
}

抽象类 / 方法的核心规则

  1. 抽象类不能实例化:永远不能写 new 抽象类名()(比如new Animal()),只能通过子类创建对象;
  2. 子类必须重写抽象方法:如果子类不重写抽象父类的所有抽象方法,那么子类也必须定义为抽象类(abstract class Bird extends Animal {});
  3. 抽象类可以有普通成员:抽象类不是 “空类”,可以有普通成员变量、普通方法、构造方法(供子类初始化父类部分);
  4. 抽象方法没有方法体:抽象方法只能写 abstract 返回值 方法名(参数);,不能加{},加了会编译报错;
  5. 抽象类的核心价值:作为 “规范父类”,强制所有子类实现统一的方法(比如所有动物都必须实现eat()),避免子类漏写核心方法。

4.10 面向抽象编程

面向抽象编程 = 写代码时,只依赖抽象类 / 接口(比如 Animal 抽象类),而不依赖具体的子类(比如 Dog、Cat)。所有变量、方法参数 / 返回值都用抽象类型,逻辑只关心 “抽象规定的行为”,不关心 “子类的具体实现”。

反面例子:面向具体类(扩展性差)

// 抽象父类(之前学的Animal)
abstract class Animal {
    abstract void eat();
}
class Dog extends Animal { @Override void eat() {System.out.println("狗啃骨头");} }
class Cat extends Animal { @Override void eat() {System.out.println("猫吃鱼");} }

// 面向具体类的方法:新增Bird时,必须改这个方法
public static void feedDog(Dog dog) { dog.eat(); }
public static void feedCat(Cat cat) { cat.eat(); }

// 测试:新增Bird子类后,要新增feedBird()方法,改老代码!
public static void main(String[] args) {
    feedDog(new Dog());
    feedCat(new Cat());
    // 新增Bird后,必须加:public static void feedBird(Bird bird) { ... }
}

正面例子:面向抽象编程(扩展性好)

// 核心:方法参数用抽象类Animal,而非具体子类
public static void feed(Animal animal) { 
    animal.eat(); // 只关心Animal有eat()行为,不关心具体是Dog/Cat/Bird
}

// 测试:新增子类不用改feed()方法,直接传参即可
public static void main(String[] args) {
    feed(new Dog());   // 输出:狗啃骨头
    feed(new Cat());   // 输出:猫吃鱼
    feed(new Bird());  // 新增Bird子类后,直接传参,不用改feed()!
}

// 新增Bird子类(仅扩展,不改老代码)
class Bird extends Animal {
    @Override void eat() { System.out.println("鸟吃虫子"); }
}

核心优势

  • 代码更稳定:老代码(比如 feed ())不用改,避免改出 bug;
  • 扩展更简单:新增功能只需要加子类,不用动原有逻辑;
  • 逻辑更清晰:只关注 “抽象规定的行为”(比如 Animal 要 eat ()),不用纠结具体实现。

4.11 开-闭源原则

开闭原则 = 对扩展开放(Open for extension),对修改关闭(Closed for modification)

总结

  1. 面向抽象编程:基于抽象类 / 接口写代码,变量 / 方法参数用抽象类型,不依赖具体子类 —— 核心是 “只关心规则,不关心实现”;
  2. 开闭原则:对扩展开放(新增子类)、对修改关闭(不改老代码)—— 是面向对象编程的核心准则;
  3. 核心关系:面向抽象编程是实现开闭原则的关键手段,两者都依托继承 / 抽象类 / 多态实现;
  4. 最终价值:让代码更稳定、易扩展,避免新增功能时改乱老代码。

五、接口与实现

5.1 接口

// 1. 定义接口:行为规范(能跑)
interface Runable {
    // 接口的方法:默认是 public abstract(初学者可以不用写,编译器会自动加)
    void run(); // 只定规则:必须实现“跑”的行为,无方法体
}

// 2. 类实现接口:用implements关键字,必须重写所有抽象方法
class Car implements Runable {
    // 履行接口契约:重写run(),实现汽车跑的逻辑
    @Override
    public void run() { // 注意:必须加public(接口方法默认public,子类不能更严格)
        System.out.println("汽车靠发动机跑,速度80km/h");
    }
}

// 另一个类实现接口:不同的实现逻辑
class Bicycle implements Runable {
    @Override
    public void run() {
        System.out.println("自行车靠人力蹬,速度15km/h");
    }
}

// 测试:接口的核心价值——多态(接口引用指向实现类对象)
public class Test {
    public static void main(String[] args) {
        // 错误!接口不能创建对象
        // Runable runable = new Runable();
        
        // 正确:接口引用指向实现类对象(类似抽象类的上转型对象)
        Runable car = new Car();
        car.run(); // 输出:汽车靠发动机跑,速度80km/h
        
        Runable bike = new Bicycle();
        bike.run(); // 输出:自行车靠人力蹬,速度15km/h
    }
}

核心规则

  1. 接口不能实例化:永远不能写 new 接口名()(比如new Runable()),只能通过 “接口引用指向实现类对象” 使用;
  2. 实现类必须重写所有抽象方法:如果类不重写接口的所有抽象方法,这个类必须定义为抽象类(abstract class Plane implements Runable {});
  3. 接口的默认规则(新手不用记,但要知道)
  • 接口里的方法:默认是 public abstract(哪怕你只写void run();,编译器也会自动加);
  • 接口里的变量:默认是 public static final(常量),比如interface A { int NUM = 10; } 等价于public static final int NUM = 10;

5.3 接口回调

接口回调 = 调用方持有接口引用(指向实现类对象),在需要的时候调用接口方法,实际执行的是实现类的具体逻辑

// 1. 定义回调接口:规定“点击后要做什么”(只有规范,无实现)
interface OnClickListener {
    // 回调方法:按钮点击后要执行的逻辑(由实现类决定)
    void onClick();
}

// 2. 调用方:按钮类(持有接口引用,时机到了调用回调方法)
class Button {
    // 保存接口引用(指向实现类对象)
    private OnClickListener listener;

    // 给按钮设置“回调逻辑”(传入接口实现类对象)
    public void setOnClickListener(OnClickListener listener) {
        this.listener = listener;
    }

    // 模拟按钮被点击(触发回调的时机)
    public void click() {
        System.out.println("按钮被点击了");
        // 时机成熟:回调接口方法(实际执行实现类的逻辑)
        if (listener != null) {
            listener.onClick();
        }
    }
}

// 3. 实现类:自定义点击后的逻辑(比如“打开弹窗”)
class PopupClickListener implements OnClickListener {
    @Override
    public void onClick() {
        // 回调执行的具体逻辑
        System.out.println("点击后:打开弹窗");
    }
}

// 测试:演示接口回调的完整流程
public class Test {
    public static void main(String[] args) {
        // 步骤1:创建调用方(按钮)
        Button button = new Button();
        
        // 步骤2:创建接口实现类(自定义回调逻辑)
        OnClickListener listener = new PopupClickListener();
        
        // 步骤3:把实现类对象传给调用方(给按钮绑定点击逻辑)
        button.setOnClickListener(listener);
        
        // 步骤4:触发时机(点击按钮),调用方自动回调接口方法
        button.click();
        // 输出:
        // 按钮被点击了
        // 点击后:打开弹窗
    }
}

接口回调的核心步骤:

  1. 定义回调接口:声明要 “回调的方法”(比如OnClickListeneronClick());
  2. 定义调用方:持有接口引用,提供 “设置接口对象” 的方法,以及 “触发回调的时机”(比如Buttonclick()方法);
  3. 实现回调接口:自定义回调方法的具体逻辑(比如PopupClickListeneronClick());
  4. 绑定 + 触发:把实现类对象传给调用方,触发时机到了,调用方自动回调接口方法。

5.4 函数接口与Lambda表达式

函数式接口 = 只有一个抽象方法 的接口(可以有默认方法、静态方法,不影响),是专门为 Lambda 表达式设计的接口。

// @FunctionalInterface:标记这是函数式接口(可选,但推荐加)
@FunctionalInterface
interface OnClickListener {
    // 只有1个抽象方法(符合函数式接口规则)
    void onClick(); 
    
    // 可以有默认方法(不影响函数式接口的判定)
    default void showTip() {
        System.out.println("点击提示");
    }
}

// 错误示例:有2个抽象方法,加@FunctionalInterface会编译报错
// @FunctionalInterface
// interface TestInterface {
//     void a();
//     void b();
// }

Lambda 表达式的核心作用是:替代函数式接口的 “匿名内部类实现”,把几十行的冗余代码简化成一行,只保留 “参数” 和 “方法体核心逻辑”。

“传统写法”(匿名内部类)—— 繁琐:

public class Test {
    public static void main(String[] args) {
        // 传统:匿名内部类实现函数式接口
        OnClickListener listener = new OnClickListener() {
            @Override
            public void onClick() {
                System.out.println("按钮被点击了(匿名内部类)");
            }
        };
        listener.onClick(); // 执行逻辑
    }
}

Lambda 替代匿名内部类,直接保留 “参数” 和 “方法体”,代码大幅简化:

public class Test {
    public static void main(String[] args) {
        // Lambda写法:() -> { 方法体 }
        OnClickListener listener = () -> {
            System.out.println("按钮被点击了(Lambda)");
        };
        listener.onClick(); // 输出:按钮被点击了(Lambda)
    }
}

Lambda 的通用语法:(参数列表) -> { 方法体 },根据场景可简化:

带参数的函数式接口 + Lambda(更实用的场景):

// 带参数的函数式接口(计算两个数的和)
@FunctionalInterface
interface Calculator {
    int add(int a, int b); // 1个抽象方法,带2个参数
}

public class Test {
    public static void main(String[] args) {
        // Lambda实现:(a,b)是参数,-> 后面是方法体(返回a+b)
        Calculator calculator = (a, b) -> {
            return a + b;
        };
        System.out.println(calculator.add(2, 3)); // 输出5
        
        // 简化写法:单返回语句,省略大括号和return
        Calculator simpleCalc = (a, b) -> a + b;
        System.out.println(simpleCalc.add(4, 5)); // 输出9
    }
}

函数式接口与 Lambda 的核心关联:

  1. Lambda 只能用于函数式接口:如果接口有多个抽象方法,Lambda 无法确定要实现哪个,会直接编译报错(这也是函数式接口的意义);
  2. Lambda 不是 “新对象”:本质是简化匿名内部类的语法糖(编译器会自动转换为匿名内部类),但代码更简洁;
  3. 核心价值:去掉匿名内部类的 “模板代码”(比如new 接口() { @Override ... }),只保留核心逻辑,让代码更简洁、易读。

易踩坑:

  1. 误区:“只要是接口就能用 Lambda”→ 必须是只有 1 个抽象方法的函数式接口;
  2. 误区:“Lambda 能替代所有匿名内部类”→ 只针对函数式接口,普通接口(多抽象方法)不行;
  3. 简化规则:只有 “方法体只有一行” 时,才能省略大括号 /return;参数只有一个时,才能省略小括号。

5.5 接口与多态

接口多态 = 接口类型的引用变量,指向该接口的任意实现类对象,调用接口方法时,实际执行的是实现类重写的逻辑

// 1. 定义接口(多态的“通用规范”)
interface Flyable {
    // 接口的抽象方法(多态的核心:实现类必须重写)
    void fly();
}

// 2. 实现类1:Bird实现Flyable,重写fly()
class Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("鸟靠翅膀扇动飞,飞得灵活");
    }
}

// 2. 实现类2:Plane实现Flyable,重写fly()
class Plane implements Flyable {
    @Override
    public void fly() {
        System.out.println("飞机靠引擎推动飞,飞得快");
    }
}

// 3. 接口多态的核心:接口引用指向不同实现类对象
public class Test {
    // 统一的方法:接收接口类型(而非具体实现类)
    public static void letItFly(Flyable flyable) {
        flyable.fly(); // 调用接口方法,执行结果由实现类决定
    }

    public static void main(String[] args) {
        // 接口引用指向Bird对象(类似继承多态的“上转型对象”)
        Flyable bird = new Bird();
        letItFly(bird); // 输出:鸟靠翅膀扇动飞,飞得灵活
        
        // 接口引用指向Plane对象
        Flyable plane = new Plane();
        letItFly(plane); // 输出:飞机靠引擎推动飞,飞得快
        
        // 直接传实现类对象,自动适配接口类型
        letItFly(new Bird()); // 输出同上
        letItFly(new Plane()); // 输出同上
    }
}

核心特征

  1. 核心形式接口名 引用变量 = new 实现类名();(比如Flyable bird = new Bird();)—— 和继承多态的父类引用指向子类对象完全对应;
  2. 调用规则:调用接口方法时,“看实现类不看接口引用”(和继承多态 “看子类不看父类引用” 一致):
    1. 哪怕用接口引用调用方法,执行的也是实现类重写的逻辑;
    2. 接口只定义 “要做什么”,实现类定义 “怎么做”;
  3. 支持 “多实现” 的多态(接口独有的优势):类可以实现多个接口,一个对象能同时作为多个接口的 “多态对象”。
    // 新增“能跑”接口
    interface Runable {
        void run();
    }
    
    // 实现两个接口:Plane既会飞又会跑
    class Plane implements Flyable, Runable {
        @Override
        public void fly() { System.out.println("飞机飞"); }
        @Override
        public void run() { System.out.println("飞机在跑道滑行"); }
    }
    
    // 同一个Plane对象,可作为不同接口的多态对象
    public static void main(String[] args) {
        Plane plane = new Plane();
        Flyable f = plane; // 作为Flyable的多态对象
        Runable r = plane; // 作为Runable的多态对象
        f.fly(); // 飞机飞
        r.run(); // 飞机在跑道滑行
    }
  4. 接口引用的限制:只能调用接口中定义的方法,不能调用实现类的特有方法(和继承多态 “父类引用不能调子类特有方法” 一致):
class Bird implements Flyable {
    @Override
    public void fly() {}
    // 特有方法
    public void sing() { System.out.println("鸟唱歌"); }
}

public static void main(String[] args) {
    Flyable bird = new Bird();
    bird.fly(); // 正常执行
    // bird.sing(); // 报错!Flyable接口没有sing()方法
}

5.6 接口参数

接口参数 = 方法的参数类型不是 “具体实现类”(比如 Bird、Plane),而是 “接口”(比如 Flyable),调用方法时可以传入任意该接口的实现类对象

// 1. 定义接口(行为规范)
interface Runable {
    void run(); // 所有“能跑”的事物都要实现这个方法
}

// 2. 实现类1:Car实现Runable
class Car implements Runable {
    @Override
    public void run() {
        System.out.println("汽车靠发动机跑");
    }
}

// 2. 实现类2:Bicycle实现Runable
class Bicycle implements Runable {
    @Override
    public void run() {
        System.out.println("自行车靠人力蹬");
    }
}

// 3. 定义带接口参数的方法(核心!参数类型是接口Runable)
public class Test {
    // 方法参数是接口类型,而非具体类
    public static void startRun(Runable runable) {
        System.out.println("启动运行:");
        runable.run(); // 调用接口方法,执行实现类的具体逻辑
    }

    public static void main(String[] args) {
        // 传入Car对象(自动适配接口参数,体现多态)
        startRun(new Car()); 
        // 输出:启动运行:汽车靠发动机跑
        
        // 传入Bicycle对象(同一个方法,适配不同实现类)
        startRun(new Bicycle()); 
        // 输出:启动运行:自行车靠人力蹬
    }
}

5.7 abstract类与接口的比较

为什么要用接口?

  1. 弥补单继承:Java 中类只能继承一个父类,但可以实现多个接口(比如 Plane 既实现 Runable 又实现 Flyable);
  2. 统一行为规范:不同类(Car、Bicycle、Plane)都能 “跑”,但实现逻辑不同,接口能统一 “跑” 的行为名称,方便面向抽象编程;
  3. 解耦:写代码时只依赖接口(Runable),不依赖具体类(Car),新增实现类(比如 Train)不用改老代码,符合开闭原则。

六、内部类与异常类

6.1 内部类

内部类是定义在另一个类(外部类)里面的类

成员内部类

写在外部类的「成员位置」(类中、方法外),和外部类的成员变量 / 方法同级

// 外部类:汽车
class Car {
    // 外部类私有成员
    private String fuel = "汽油";

    // 成员内部类:发动机(写在Car的成员位置)
    class Engine {
        // 内部类能直接访问外部类的私有变量
        public void work() {
            System.out.println("发动机使用" + fuel + "驱动");
        }
    }

    // 外部类访问内部类:需要创建内部类对象
    public void startCar() {
        Engine engine = new Engine();
        engine.work();
    }
}

// 测试
public class Test {
    public static void main(String[] args) {
        // 步骤1:创建外部类对象
        Car car = new Car();
        // 步骤2:通过外部类对象创建内部类对象(格式:外部类对象.new 内部类())
        Car.Engine engine = car.new Engine();
        engine.work(); // 输出:发动机使用汽油驱动

        // 也可以直接调用外部类方法(间接使用内部类)
        car.startCar(); // 输出同上
    }
}

局部内部类

写在外部类的「方法 / 代码块内」,只在该方法内有效

class Car {
    public void run() {
        // 方法内的局部变量
        int speed = 80;

        // 局部内部类:轮子(只在run方法内有效)
        class Wheel {
            public void roll() {
                // 能访问外部类方法的局部变量
                System.out.println("轮子以" + speed + "km/h滚动");
            }
        }

        // 只能在方法内创建局部内部类对象并使用
        Wheel wheel = new Wheel();
        wheel.roll();
    }
}

// 测试
public class Test {
    public static void main(String[] args) {
        Car car = new Car();
        car.run(); // 输出:轮子以80km/h滚动
        // 无法在外部访问Wheel类(作用域仅限run方法)
    }
}

匿名内部类

没有名字的内部类,直接继承类 / 实现接口,一次性使用(用完就丢);

// 定义接口
interface Runable {
    void run();
}

public class Test {
    public static void main(String[] args) {
        // 匿名内部类:直接实现Runable接口,没有类名
        Runable car = new Runable() {
            @Override
            public void run() {
                System.out.println("汽车跑");
            }
        };
        car.run(); // 输出:汽车跑

        // 更常用的场景:创建线程(Runnable是接口)
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程执行");
            }
        }).start(); // 输出:线程执行
    }
}

误区

  1. 误区:“成员内部类能直接 new”→ 不行!必须先创建外部类对象,再通过外部类对象 new 内部类(car.new Engine());
  2. 误区:“局部内部类能加 public”→ 不行!局部内部类不能加任何访问修饰符;
  3. 匿名内部类:只能用一次,没有类名,无法重复创建对象(想复用就写普通类 / 用 Lambda)。

6.2 匿名类

匿名类 = 没有类名的内部类,创建对象时直接「继承某个类 / 实现某个接口」,并当场重写核心方法,用完即丢,无法复用

实现接口

// 1. 定义一个简单接口
interface Writeable {
    void write(); // 只有一个抽象方法
}

// ---------------------- 传统写法(繁琐) ----------------------
// 步骤1:写完整的实现类
class Pen implements Writeable {
    @Override
    public void write() {
        System.out.println("钢笔写字");
    }
}

// 步骤2:创建对象使用
public class Test {
    public static void main(String[] args) {
        Writeable pen = new Pen();
        pen.write(); // 输出:钢笔写字
    }
}

// ---------------------- 匿名类写法(极简) ----------------------
public class Test {
    public static void main(String[] args) {
        // 直接创建匿名类对象:没有类名,当场实现接口+重写方法
        Writeable pen = new Writeable() {
            // 当场重写接口的抽象方法
            @Override
            public void write() {
                System.out.println("钢笔写字");
            }
        };
        pen.write(); // 输出:钢笔写字
    }
}
  1. 匿名类核心:无类名、一次性使用的内部类,创建时直接实现接口 / 继承类并重写方法;
  2. 核心写法new 接口/父类() { 重写方法 },适合临时使用的场景;
  3. 核心价值:简化代码,少写模板类,是新手实现接口 / 继承类的 “快捷方式”;
  4. 关键关联:Lambda 表达式是匿名类在「函数式接口」场景下的进一步简化(只有一个抽象方法的接口,用 Lambda 更短)。

6.3 异常类

异常类 = 描述程序 “非正常情况”(比如空指针、数组越界、文件找不到)的类,Java 把不同类型的错误封装成不同的异常类,比如空指针错误对应NullPointerException类,数组越界对应ArrayIndexOutOfBoundsException类。

分类

异常类型通俗理解常见例子处理要求
运行时异常(非检查异常)编译时不提醒,运行时才出现的错误NullPointerException(空指针)、ArrayIndexOutOfBoundsException(数组越界)不强制处理(可处理也可不处理)
编译时异常(检查异常)编译时就必须处理的错误IOException(文件读写错误)、SQLException(数据库错误)必须处理(否则编译报错)
public class Test {
    public static void main(String[] args) {
        // 1. 运行时异常(编译不报错,运行才报错)
        String str = null;
        // str.length():空指针,运行时抛出NullPointerException
        // System.out.println(str.length()); 

        // 2. 编译时异常(编译就报错,必须处理)
        // Thread.sleep(1000):休眠,抛出InterruptedException(编译时异常)
        try { // 必须用try-catch包裹,否则编译报错
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace(); // 打印异常信息
        }
    }
}

基本使用

1. 捕获异常(try-catch):“接住” 错误,不让程序崩溃

核心语法:try{ 可能出错的代码 } catch(异常类 变量名){ 处理错误 },相当于 “预判哪里会出错,出错后怎么处理”。

public static void main(String[] args) {
    int[] arr = {1,2,3};
    try {
        // 可能出错的代码:数组只有3个元素,索引3越界
        System.out.println(arr[3]); 
    } catch (ArrayIndexOutOfBoundsException e) {
        // 出错后的处理:打印错误信息,程序继续运行
        System.out.println("出错了:数组索引越界");
        e.printStackTrace(); // 打印详细错误(原因、代码行)
    }
    System.out.println("程序没崩溃,继续执行"); // 这行能正常输出
}

2. 抛出异常(throws):“甩锅” 给调用者处理

如果当前方法不想处理异常,可在方法名后加throws 异常类,把异常抛给调用该方法的代码处理(适合编译时异常)。

// 方法抛出异常,让调用者处理
public static void sleep() throws InterruptedException {
    Thread.sleep(1000); // 编译时异常,不处理,直接抛出
}

public static void main(String[] args) {
    try {
        sleep(); // 调用抛出异常的方法,必须处理
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

6.4 断言

断言(assert是 Java 关键字)= 代码中添加的 “自检条件”,语法是 assert 条件; 或 assert 条件 : "提示信息";

  • 如果「条件为 true」:程序正常执行,断言相当于 “隐形代码”,不影响逻辑;
  • 如果「条件为 false」:立刻抛出AssertionError(属于 Error 类),中断程序运行,并告诉你哪里错了。

1. 基础用法:仅检查条件

public class Test {
    public static void main(String[] args) {
        int age = -1; // 错误的年龄值(调试阶段的问题)
        
        // 断言:检查年龄是否≥0(假设年龄不可能为负)
        assert age >= 0;
        
        System.out.println("年龄是:" + age);
    }
}

2. 带提示信息:更易定位问题

public class Test {
    public static void main(String[] args) {
        int age = -1;
        
        // 断言:条件不成立时,抛出的错误会包含提示信息
        assert age >= 0 : "年龄不能为负数,当前值:" + age;
        
        System.out.println("年龄是:" + age);
    }
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐