【JavaSE】类和对象基础

写到这里终于到了 Java 学习中最核心的部分了。如果说变量、循环、条件判断是 Java 的"砖瓦",那么类和对象就是 Java 的"建筑设计图"。掌握了类和对象,你才算真正入门了 Java 这门面向对象的语言。

话不多说,开始吧!

一、为什么需要类和对象?

在学习类和对象之前,我们先思考一个问题:假设你要管理 3 个学生的信息(姓名、年龄、学号),用我们之前学过的知识,你会怎么做?

可能会这样写:

java 复制代码
String name1 = "张三";
int age1 = 20;
String id1 = "2024001";

String name2 = "李四";
int age2 = 21;
String id2 = "2024002";

String name3 = "王五";
int age3 = 19;
String id3 = "2024003";

看起来没问题,但如果要管理 100 个学生呢?300 个呢?而且这些数据是分散的,如果要给某个学生的信息做个操作(比如打印信息、修改年龄),代码会非常混乱。

这就是为什么我们需要类和对象。类就像一个模板,定义了一类事物具有哪些属性和行为;对象就是根据这个模板创建出来的具体实例。用类和对象,上面的代码可以变成这样:

java 复制代码
Student s1 = new Student("张三", 20, "2024001");
Student s2 = new Student("李四", 21, "2024002");
Student s3 = new Student("王五", 19, "2024003");

是不是清爽多了?接下来我们就来看看怎么实现。


二、类的定义和对象的创建

2.1 什么是类?

类是 Java 中最基本的组成单位。你可以把类理解为一张"设计图纸"或"模板"。比如汽车工厂的设计图,上面标注了汽车有哪些零件(属性)、能做什么(方法)。

在 Java 中,用 class 关键字定义一个类,语法格式如下:

java 复制代码
public class 类名 {
    // 成员变量(属性)
    // 构造方法
    // 成员方法(行为)
}

我们来定义一个简单的 Person 类:

java 复制代码
public class Person {
    // 成员变量
    String name;
    int age;
    
    // 成员方法
    public void sayHello() {
        System.out.println("大家好,我叫" + name + ",今年" + age + "岁。");
    }
}

这个 Person 类定义了两个属性(name 和 age)和一个方法(sayHello)。但要注意,类只是一个模板,它本身不占用实际的内存空间,只有创建对象后才会分配内存 。

2.2 什么是对象?

对象是类的实例化结果,是实实在在存在的东西。还是用汽车的例子,类是设计图,对象就是根据设计图生产出来的一辆辆真实的汽车。

创建对象使用 new 关键字,语法如下:

java 复制代码
类名 对象名 = new 类名();

我们来创建几个 Person 对象:

java 复制代码
public class PersonTest {
    public static void main(String[] args) {
        // 创建第一个 Person 对象
        Person p1 = new Person();
        p1.name = "张三";
        p1.age = 25;
        p1.sayHello();  // 输出:大家好,我叫张三,今年25岁。
        
        // 创建第二个 Person 对象
        Person p2 = new Person();
        p2.name = "李四";
        p2.age = 30;
        p2.sayHello();  // 输出:大家好,我叫李四,今年30岁。
    }
}

可以看到,虽然 p1 和 p2 都是 Person 类的对象,但它们是相互独立的,有各自的属性值。这就是对象的特点:同一个类可以创建多个对象,每个对象都有自己的状态(属性值)。


三、成员变量

成员变量就是类的属性,用来描述对象的特征。比如汽车有颜色、品牌、速度等属性。

3.1 成员变量的定义

我们来定义一个 Car 类:

java 复制代码
public class Car {
    // 成员变量
    String brand;      // 品牌
    String color;      // 颜色
    double speed;      // 速度
    
    // 展示汽车信息的方法
    public void showInfo() {
        System.out.println("品牌:" + brand);
        System.out.println("颜色:" + color);
        System.out.println("速度:" + speed + " km/h");
    }
}

3.2 成员变量的默认值

如果在定义成员变量时没有赋初始值,Java 会自动给它赋一个默认值:

  • 整数类型(byte、short、int、long):0
  • 浮点类型(float、double):0.0
  • 字符类型(char):'\u0000'(空字符)
  • 布尔类型(boolean):false
  • 引用类型(String、数组、对象等):null

来测试一下:

java 复制代码
public class CarTest {
    public static void main(String[] args) {
        Car car = new Car();
        car.showInfo();
        // 输出:
        // 品牌:null
        // 颜色:null
        // 速度:0.0
    }
}

3.3 成员变量 vs 局部变量

这里要特别注意区分成员变量和局部变量:

区别点 成员变量 局部变量
定义位置 类中,方法外 方法中或方法的参数
默认值 有默认值 没有默认值,必须赋值后才能使用
生命周期 对象创建时产生,对象销毁时消失 方法调用时产生,方法结束时消失
作用范围 整个类 定义它的方法内

举个例子:

java 复制代码
public class VariableTest {
    int a = 10;  // 成员变量
    
    public void test() {
        int b = 20;  // 局部变量
        System.out.println(a);  // 可以访问成员变量
        System.out.println(b);  // 可以访问局部变量
    }
    
    public void test2() {
        System.out.println(a);  // 可以访问成员变量
        // System.out.println(b);  // 错误!无法访问 test() 方法中的局部变量
    }
}

四、成员方法

成员方法定义了类的行为,也就是对象能做什么。比如汽车可以启动、加速、刹车等。

4.1 方法的定义

方法的语法格式如下:

java 复制代码
访问修饰符 返回值类型 方法名(参数列表) {
    // 方法体
    return 返回值;  // 如果返回值类型不是 void
}

我们来定义一个 Calculator 类:

java 复制代码
public class Calculator {
    // 加法
    public int add(int a, int b) {
        return a + b;
    }
    
    // 减法
    public int subtract(int a, int b) {
        return a - b;
    }
    
    // 乘法
    public double multiply(double a, double b) {
        return a * b;
    }
    
    // 除法
    public double divide(double a, double b) {
        if(b == 0) {
            System.out.println("除数不能为0!");
            return 0;
        }
        return a / b;
    }
}

4.2 方法的调用

使用"对象名.方法名(参数)"的方式调用方法:

java 复制代码
public class CalculatorTest {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        
        int sum = calc.add(10, 20);
        System.out.println("10 + 20 = " + sum);  // 输出:10 + 20 = 30
        
        double result = calc.divide(10.0, 3.0);
        System.out.println("10.0 / 3.0 = " + result);  // 输出:10.0 / 3.0 = 3.333...
    }
}

4.3 方法重载

Java 允许在同一个类中定义多个同名方法,只要它们的参数列表不同(参数个数、类型或顺序不同),这叫做方法重载(Overload)。

java 复制代码
public class Calculator {
    // 两个整数相加
    public int add(int a, int b) {
        return a + b;
    }
    
    // 三个整数相加
    public int add(int a, int b, int c) {
        return a + b + c;
    }
    
    // 两个小数相加
    public double add(double a, double b) {
        return a + b;
    }
}

调用时,Java 会根据传入的参数自动选择合适的方法:

java 复制代码
Calculator calc = new Calculator();
System.out.println(calc.add(1, 2));          // 调用第一个方法,输出:3
System.out.println(calc.add(1, 2, 3));       // 调用第二个方法,输出:6
System.out.println(calc.add(1.5, 2.5));      // 调用第三个方法,输出:4.0

五、构造方法

构造方法(Constructor)是一个特殊的方法,专门用于创建对象时初始化对象的属性。

5.1 构造方法的特点

  1. 方法名必须与类名完全相同
  2. 没有返回值类型(连 void 都不用写)
  3. 在使用 new 创建对象时自动调用

5.2 无参构造方法

如果你不写任何构造方法,Java 会自动提供一个无参构造方法。但只要你写了任何一个构造方法,Java 就不会再提供默认的无参构造方法了。

我们来定义一个 Phone 类:

java 复制代码
public class Phone {
    String brand;
    String model;
    double price;
    
    // 无参构造方法
    public Phone() {
        System.out.println("创建了一个手机对象");
    }
    
    public void showInfo() {
        System.out.println("品牌:" + brand + ",型号:" + model + ",价格:" + price);
    }
}

测试一下:

java 复制代码
public class PhoneTest {
    public static void main(String[] args) {
        Phone phone = new Phone();  // 输出:创建了一个手机对象
        phone.brand = "Apple";
        phone.model = "iPhone 15";
        phone.price = 5999.0;
        phone.showInfo();  // 输出:品牌:Apple,型号:iPhone 15,价格:5999.0
    }
}

5.3 有参构造方法

有参构造方法可以在创建对象时直接传入初始值,更加方便:

java 复制代码
public class Phone {
    String brand;
    String model;
    double price;
    
    // 无参构造方法
    public Phone() {
        System.out.println("创建了一个手机对象");
    }
    
    // 有参构造方法
    public Phone(String brand, String model, double price) {
        this.brand = brand;
        this.model = model;
        this.price = price;
        System.out.println("创建了一个" + brand + "手机");
    }
    
    public void showInfo() {
        System.out.println("品牌:" + brand + ",型号:" + model + ",价格:" + price);
    }
}

现在可以这样创建对象:

java 复制代码
public class PhoneTest {
    public static void main(String[] args) {
        // 使用无参构造方法
        Phone phone1 = new Phone();
        phone1.brand = "Samsung";
        phone1.model = "Galaxy S24";
        phone1.price = 6999.0;
        
        // 使用有参构造方法(更简洁)
        Phone phone2 = new Phone("Xiaomi", "14 Pro", 4999.0);
        
        phone1.showInfo();
        phone2.showInfo();
    }
}

5.4 构造方法重载

和普通方法一样,构造方法也可以重载:

java 复制代码
public class Phone {
    String brand;
    String model;
    double price;
    
    // 无参构造
    public Phone() {
        this("未知品牌", "未知型号", 0.0);  // 调用三参数构造方法
    }
    
    // 一个参数的构造
    public Phone(String brand) {
        this(brand, "未知型号", 0.0);
    }
    
    // 两个参数的构造
    public Phone(String brand, String model) {
        this(brand, model, 0.0);
    }
    
    // 三个参数的构造
    public Phone(String brand, String model, double price) {
        this.brand = brand;
        this.model = model;
        this.price = price;
    }
}

这里出现了 this(),这是用来调用本类中其他构造方法的语法,后面具体介绍。


六、this 关键字

简单来说,this 代表当前对象的引用。

我们来看一个例子:

java 复制代码
public class Box {
    int width;
    int height;
    
    public Box(int width, int height) {
        this.width = width;
        this.height = height;
    }
    
    public void showThis() {
        System.out.println("this 的值:" + this);
    }
}

public class BoxTest {
    public static void main(String[] args) {
        Box box1 = new Box(10, 20);
        Box box2 = new Box(30, 40);
        
        System.out.println("box1 的值:" + box1);
        box1.showThis();  // 输出的地址和 box1 相同
        
        System.out.println("box2 的值:" + box2);
        box2.showThis();  // 输出的地址和 box2 相同
    }
}

输出结果类似:

复制代码
box1 的值:Box@15db9742
this 的值:Box@15db9742
box2 的值:Box@6d06d69c
this 的值:Box@6d06d69c

可以看到,this 实际上就是当前对象的内存地址。当 box1 调用方法时,this 就是 box1 的地址;当 box2 调用方法时,this 就是 box2 的地址。

6.1 基本用法一:区分成员变量和局部变量

当成员变量和局部变量(或方法参数)同名时,使用 this 来区分:

java 复制代码
public class Person {
    String name;
    
    public void setName(String name) {
        // name = name;  // 这样写是错的!相当于把参数赋值给参数自己
        this.name = name;  // this.name 指的是成员变量,name 指的是参数
    }
}

6.2 基本用法二:在方法中调用其他方法

java 复制代码
public class Person {
    String name;
    int age;
    
    public void setName(String name) {
        this.name = name;
    }
    
    public void setAge(int age) {
        this.age = age;
    }
    
    public void init(String name, int age) {
        this.setName(name);  // 调用本类的 setName 方法
        this.setAge(age);    // 调用本类的 setAge 方法
        // 其实这里的 this 可以省略,直接写 setName(name) 也行
    }
}

6.3 基本用法三:作为方法的返回值

this 可以作为返回值,返回当前对象的引用。这在链式调用中非常有用:

java 复制代码
public class Person {
    String name;
    int age;
    
    public Person setName(String name) {
        this.name = name;
        return this;  // 返回当前对象
    }
    
    public Person setAge(int age) {
        this.age = age;
        return this;
    }
    
    public void showInfo() {
        System.out.println("姓名:" + name + ",年龄:" + age);
    }
}

这样就可以实现链式调用:

java 复制代码
Person p = new Person();
p.setName("张三").setAge(25).showInfo();  // 输出:姓名:张三,年龄:25

6.4 高级用法:this() 调用构造方法

前面我们在 Phone 类中见过了,this() 可以调用本类的其他构造方法,但必须放在构造方法的第一行:

java 复制代码
public class Student {
    String name;
    int age;
    String id;
    
    public Student() {
        this("未知", 0, "000000");  // 调用三参数构造方法
    }
    
    public Student(String name) {
        this(name, 0, "000000");
    }
    
    public Student(String name, int age, String id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }
}

注意事项:

  1. this() 必须是构造方法的第一条语句
  2. 不能在一个构造方法中同时使用 this() 和 super()(super 是调用父类构造方法,后面继承章节会讲)
  3. 不能形成循环调用,比如构造方法 A 调用构造方法 B,B 又调用 A

6.5 this 的常见误区

误区1:在静态方法中使用 this

java 复制代码
public class Test {
    int a = 10;
    
    public static void method() {
        // System.out.println(this.a);  // 错误!静态方法中没有 this
    }
}

为什么?因为静态方法属于类,不属于对象,调用时可能根本没有创建对象,所以没有 this 可言。

误区2:this 可以改变对象引用

java 复制代码
public class Person {
    String name;
    
    public void changeSelf() {
        this = new Person();  // 错误!this 是 final 的,不能被重新赋值
    }
}

this 本质上是一个 final 的引用,指向当前对象,不能被修改。

误区3:混淆 this() 和 this

java 复制代码
public class Test {
    public Test() {
        this();  // 错误!this() 是调用构造方法,这里会形成递归调用自己
    }
}

this 是当前对象引用,this() 是调用构造方法,两者完全不同。

6.6 this 的底层原理

当我们写 person.setName("张三") 时,Java 编译器实际上会把代码转换成类似这样:

java 复制代码
Person.setName(person, "张三");  // 伪代码,实际不是这样写

也就是说,每个非静态方法都隐式地接收一个指向调用对象的引用作为第一个参数,这个隐式参数就是 this。

所以在 setName 方法内部,this 就是传入的 person 对象的引用。


七、static 关键字深度解析

static 是 Java 中另一个非常重要的关键字,用于声明静态成员。静态成员属于类,而不属于某个具体的对象。

7.1 静态变量

静态变量也叫类变量,用 static 修饰,被该类的所有对象共享。

java 复制代码
public class Student {
    String name;           // 实例变量(非静态变量)
    static String school;  // 静态变量
    
    public Student(String name) {
        this.name = name;
    }
    
    public void showInfo() {
        System.out.println("我叫" + name + ",就读于" + school);
    }
}

测试一下:

java 复制代码
public class StudentTest {
    public static void main(String[] args) {
        // 设置静态变量(通过类名访问,推荐)
        Student.school = "清华大学";
        
        // 创建两个学生对象
        Student s1 = new Student("张三");
        Student s2 = new Student("李四");
        
        s1.showInfo();  // 输出:我叫张三,就读于清华大学
        s2.showInfo();  // 输出:我叫李四,就读于清华大学
        
        // 修改静态变量
        Student.school = "北京大学";
        
        s1.showInfo();  // 输出:我叫张三,就读于北京大学
        s2.showInfo();  // 输出:我叫李四,就读于北京大学
    }
}

可以看到,所有 Student 对象共享同一个 school 变量。一个对象修改了 school,其他对象访问时也会看到修改后的值。

静态变量 vs 实例变量对比:

特性 静态变量 实例变量
关键字 static 无
所属 属于类 属于对象
内存分配 类加载时分配,只有一份 对象创建时分配,每个对象一份
访问方式 类名.变量名(推荐)或 对象名.变量名 对象名.变量名
生命周期 类加载到程序结束 对象创建到对象销毁

7.2 静态方法

静态方法用 static 修饰,可以直接通过类名调用,不需要创建对象。

java 复制代码
public class MathUtil {
    // 静态方法:计算两个数的最大值
    public static int max(int a, int b) {
        return a > b ? a : b;
    }
    
    // 静态方法:计算圆的面积
    public static double circleArea(double radius) {
        return Math.PI * radius * radius;
    }
}

调用静态方法:

java 复制代码
public class MathUtilTest {
    public static void main(String[] args) {
        // 直接通过类名调用,不需要创建对象
        int maxNum = MathUtil.max(10, 20);
        System.out.println("最大值:" + maxNum);  // 输出:最大值:20
        
        double area = MathUtil.circleArea(5.0);
        System.out.println("面积:" + area);  // 输出:面积:78.53981633974483
    }
}

7.3 静态代码块

静态代码块在类加载时执行,只执行一次,通常用于初始化静态变量。

java 复制代码
public class DatabaseConfig {
    static String url;
    static String username;
    static String password;
    
    // 静态代码块
    static {
        System.out.println("正在加载数据库配置...");
        url = "jdbc:mysql://localhost:3306/test";
        username = "root";
        password = "123456";
        System.out.println("数据库配置加载完成!");
    }
    
    public static void showConfig() {
        System.out.println("URL: " + url);
        System.out.println("Username: " + username);
    }
}

测试:

java 复制代码
public class ConfigTest {
    public static void main(String[] args) {
        System.out.println("程序开始");
        DatabaseConfig.showConfig();  // 第一次使用 DatabaseConfig 类时,静态代码块执行
        System.out.println("---");
        DatabaseConfig.showConfig();  // 再次调用,静态代码块不会再执行
    }
}

输出:

复制代码
程序开始
正在加载数据库配置...
数据库配置加载完成!
URL: jdbc:mysql://localhost:3306/test
Username: root
---
URL: jdbc:mysql://localhost:3306/test
Username: root

代码块执行顺序:

  1. 静态代码块:类加载时执行,只执行一次
  2. 实例代码块(普通代码块):每次创建对象时执行
  3. 构造方法:每次创建对象时执行
java 复制代码
public class CodeBlockTest {
    static {
        System.out.println("1. 静态代码块");
    }
    
    {
        System.out.println("2. 实例代码块");
    }
    
    public CodeBlockTest() {
        System.out.println("3. 构造方法");
    }
    
    public static void main(String[] args) {
        System.out.println("---创建第一个对象---");
        new CodeBlockTest();
        System.out.println("---创建第二个对象---");
        new CodeBlockTest();
    }
}

输出:

复制代码
1. 静态代码块
---创建第一个对象---
2. 实例代码块
3. 构造方法
---创建第二个对象---
2. 实例代码块
3. 构造方法

7.4 static 的内存分配机制

理解 static 的关键在于理解 Java 的内存模型。Java 内存主要分为:

  1. 方法区(Method Area):存储类信息、静态变量、常量等(JDK 8 后叫元空间 Metaspace)
  2. 堆(Heap):存储对象实例
  3. 栈(Stack):存储局部变量和方法调用

当我们定义一个类时:

  • 静态变量存储在方法区,类加载时分配,所有对象共享
  • 实例变量存储在堆中,每个对象创建时分配,每个对象独立

看一个例子:

java 复制代码
public class Counter {
    static int staticCount = 0;  // 静态变量
    int instanceCount = 0;       // 实例变量
    
    public Counter() {
        staticCount++;
        instanceCount++;
    }
    
    public void show() {
        System.out.println("静态计数:" + staticCount + ",实例计数:" + instanceCount);
    }
}

public class CounterTest {
    public static void main(String[] args) {
        Counter c1 = new Counter();
        c1.show();  // 输出:静态计数:1,实例计数:1
        
        Counter c2 = new Counter();
        c2.show();  // 输出:静态计数:2,实例计数:1
        
        Counter c3 = new Counter();
        c3.show();  // 输出:静态计数:3,实例计数:1
    }
}

为什么 staticCount 一直在增加,而 instanceCount 始终是 1?

因为:

  • staticCount 是静态变量,在方法区只有一份,三个对象共享,所以累加到 3
  • instanceCount 是实例变量,每个对象都有自己的一份,每个对象的 instanceCount 都是 1

7.5 静态方法的限制

静态方法有几个重要的限制:

限制1:静态方法不能直接访问非静态成员

java 复制代码
public class Test {
    int a = 10;           // 实例变量
    static int b = 20;    // 静态变量
    
    public static void method() {
        // System.out.println(a);  // 错误!静态方法不能访问实例变量
        System.out.println(b);     // 正确!可以访问静态变量
    }
}

为什么?因为静态方法属于类,调用时可能还没有创建对象,所以无法访问实例变量。

限制2:静态方法中不能使用 this 和 super

java 复制代码
public class Test {
    int a = 10;
    
    public static void method() {
        // System.out.println(this.a);  // 错误!静态方法中没有 this
    }
}

同样的道理,静态方法不属于对象,所以没有 this 引用。

限制3:非静态方法可以访问静态成员,但反过来不行

java 复制代码
public class Test {
    static int staticVar = 10;
    int instanceVar = 20;
    
    // 静态方法
    public static void staticMethod() {
        System.out.println(staticVar);       // 正确
        // System.out.println(instanceVar);  // 错误
        // instanceMethod();                 // 错误
    }
    
    // 非静态方法
    public void instanceMethod() {
        System.out.println(staticVar);    // 正确
        System.out.println(instanceVar);  // 正确
        staticMethod();                   // 正确
    }
}

7.6 静态导入

Java 5 引入了静态导入(static import),可以直接导入类的静态成员,使用时不需要加类名前缀:

java 复制代码
// 不使用静态导入
public class Test1 {
    public static void main(String[] args) {
        System.out.println(Math.PI);
        System.out.println(Math.abs(-10));
    }
}

// 使用静态导入
import static java.lang.Math.*;  // 导入 Math 类的所有静态成员

public class Test2 {
    public static void main(String[] args) {
        System.out.println(PI);      // 不需要写 Math.PI
        System.out.println(abs(-10)); // 不需要写 Math.abs
    }
}

静态导入语法:

  • import static 包名.类名.静态成员; // 导入单个静态成员
  • import static 包名.类名.*; // 导入所有静态成员

八、访问修饰符

Java 提供了四种访问修饰符来控制类、方法和变量的访问权限。

8.1 四种访问修饰符

修饰符 本类 同包 子类(不同包) 其他包
private ✓ ✗ ✗ ✗
default(无修饰符) ✓ ✓ ✗ ✗
protected ✓ ✓ ✓ ✗
public ✓ ✓ ✓ ✓

注意:表格中的"同包"和"不同包"指的是 Java 的包(package)机制,我们会在下一节详细讲解什么是包。现在你只需要知道,包是用来组织和管理类的一种方式。

8.2 示例代码

java 复制代码
public class AccessTest {
    private int privateVar = 1;    // 私有变量,只能在本类中访问
    int defaultVar = 2;            // 默认权限,同包可访问
    protected int protectedVar = 3; // 受保护的,同包和子类可访问
    public int publicVar = 4;       // 公有的,任何地方都可访问
    
    private void privateMethod() {
        System.out.println("私有方法");
    }
    
    void defaultMethod() {
        System.out.println("默认方法");
    }
    
    protected void protectedMethod() {
        System.out.println("受保护方法");
    }
    
    public void publicMethod() {
        System.out.println("公有方法");
    }
}

8.3 使用建议

  1. 类的成员变量通常用 private,通过 public 的 getter 和 setter 方法访问(封装,下一节详细讲)
  2. 工具类的方法用 public static,方便外部调用
  3. 构造方法通常用 public,除非实现单例模式
  4. 默认权限适合内部使用的辅助类和方法

九、包(Package)

9.1 什么是包

包(Package)本质就是文件夹,用来组织代码文件的。

9.2 为什么需要包?

假设你正在开发一个大型项目,现在有这样的情况:

java 复制代码
// 你写的 User 类
public class User {
    String name;
    String email;
}

// 同事写的 User 类(用于权限管理)
public class User {
    String username;
    String role;
}

两个类都叫 User,放在同一个项目里会直接冲突,编译都通过不了!

再看另一个问题:项目里有 50 个类文件,全部散乱地放在一个文件夹里:

复制代码
User.java
Product.java
Order.java
OrderService.java
UserService.java
ProductService.java
...

想找个类都费劲,更别说维护了。这就是为什么我们需要包。

包的三大作用:

  1. 避免类名冲突:不同包里可以有同名的类
  2. 便于管理:按功能模块组织类,结构清晰
  3. 访问控制:配合访问修饰符,控制类的可见范围

有了包,上面的问题可以解决了:

复制代码
com.shop.entity.User      // 实体类的 User
com.shop.security.User    // 权限管理的 User

两个 User 类在不同的包里,互不冲突。

  • com 为域名反写(后面说)
  • shop 为项目标识
  • entity/security 为分层架构中的一层,表示这个包的职责,在项目中才能更好理解本概念
  • User 为真正的类名

在文件系统中长这样:

复制代码
项目根目录/
└── src/
    └── com/              
        └── shop/        
            └── entity
                └── User.java  
            └── security
                └── User.java  

简而言之:

"这是 com(商业组织) 下的 shop(商城项目) 中的 entit / security 包里的 User(用户类)"


9.3 包的定义和使用

9.3.1 定义包

使用 package 关键字定义包,必须放在源文件的第一行(注释除外):

java 复制代码
package com.example.myapp;

public class MyClass {
    // 类的内容
}

这样 MyClass 就属于 com.example.myapp 包了。

语法规则:

  • package 语句必须是代码的第一行(注释可以在前面)
  • 一个类文件只能有一个 package 语句
  • 如果没有 package 语句,类属于默认包(不推荐)

来看一个完整的例子:

java 复制代码
// 文件:Person.java
package com.company.entity;

public class Person {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public void showInfo() {
        System.out.println("姓名:" + name + ",年龄:" + age);
    }
}

在 IDEA 中创建包:

步骤1:创建包

  1. 右键点击 src 目录
  2. 选择 New → Package
  3. 输入包名(如 com.example.entity)
  4. 点击 OK

步骤2:在包中创建类

  1. 右键点击刚创建的包
  2. 选择 New → Java Class
  3. 输入类名(如 User)
  4. IDEA 会自动添加 package 语句

步骤3:运行

  • 右键点击类,选择 Run 即可
  • IDEA 会自动处理编译和类路径
9.3.2 使用其他包中的类

如果要使用其他包中的类,有两种方式:

方式1:使用完全限定名(类的全名)

java 复制代码
package com.company.test;

public class Test {
    public static void main(String[] args) {
        // 使用完全限定名
        com.company.entity.Person p = new com.company.entity.Person("张三", 25);
        p.showInfo();
    }
}

这种方式很麻烦,每次都要写一长串。

方式2:使用 import 导入(推荐)

java 复制代码
package com.company.test;

import com.company.entity.Person;  // 导入 Person 类

public class Test {
    public static void main(String[] args) {
        // 直接使用类名
        Person p = new Person("张三", 25);
        p.showInfo();
    }
}

9.4 包的命名规范

包名不是随便起的,Java 有一套约定俗成的命名规范:

基本规则:

  1. 全部小写:包名必须全部是小写字母
  2. 域名反写:使用公司域名的反写形式
  3. 多级包名用点分隔 :如 com.company.project.module
  4. 避免使用 Java 关键字:不能用 int、class 等关键字

常见命名模式:

复制代码
com.公司名.项目名.模块名

示例:
com.alibaba.fastjson
com.baidu.search.core
org.springframework.boot

为什么要域名反写?

因为域名是全球唯一的,反写后可以保证包名不会和别人冲突。比如:

几个例子:

java 复制代码
// 好的命名
package com.example.shop.entity;
package com.mycompany.blog.service;
package org.apache.commons.lang;

// 不好的命名
package Shop;              // 错误:首字母不能大写
package com.example.My_App;  // 不推荐:不要用下划线
package com.123company;      // 错误:不能以数字开头

9.5 包与目录结构

包名和文件夹路径是一一对应的。这一点非常重要!

比如你定义了一个类:

java 复制代码
package com.example.shop.entity;

public class Product {
    // ...
}

那么 Product.java 文件必须放在这样的目录结构中:

复制代码
项目根目录/
└── com/
    └── example/
        └── shop/
            └── entity/
                └── Product.java

每个点(.)对应一层文件夹。如果目录结构不对,编译时会报错。

一个完整的项目结构示例:

复制代码
MyProject/
├── src/
│   └── com/
│       └── mycompany/
│           └── myapp/
│               ├── entity/
│               │   ├── User.java
│               │   └── Product.java
│               ├── service/
│               │   ├── UserService.java
│               │   └── ProductService.java
│               └── util/
│                   └── DateUtil.java
└── out/  (编译后的 .class 文件)

9.6 import 语句详解

9.6.1 import 的基本用法

import 语句用来导入其他包中的类,必须放在 package 语句之后、类定义之前。

java 复制代码
package com.example.test;           // package 必须在第一行

import com.example.entity.User;     // import 在 package 之后
import com.example.entity.Product;
import java.util.Scanner;
import java.util.ArrayList;

public class Test {                 // 类定义在最后
    // ...
}
9.6.2 导入单个类 vs 导入整个包

导入单个类:

java 复制代码
import java.util.Scanner;
import java.util.ArrayList;
import java.util.HashMap;

导入整个包(使用 * 通配符):

java 复制代码
import java.util.*;  // 导入 java.util 包下的所有类

两种方式都可以,但要注意:

  • * 只能导入当前包下的类,不能导入子包
  • * 不会影响性能(编译时会展开),但可读性稍差
java 复制代码
import java.util.*;      // 只导入 java.util 包下的类
import java.util.List;   // 正确:List 在 java.util 包下

// 错误示例
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;  // 还需要单独导入,因为它在子包里
9.6.3 解决类名冲突

如果两个包中有同名的类,怎么办?

java 复制代码
import java.util.Date;
import java.sql.Date;  // 冲突!两个包都有 Date 类

解决方法1:只导入一个,另一个用完全限定名

java 复制代码
import java.util.Date;  // 导入这个

public class Test {
    public static void main(String[] args) {
        Date date1 = new Date();  // java.util.Date
        java.sql.Date date2 = new java.sql.Date(System.currentTimeMillis());  // 用全名
    }
}

解决方法2:都用完全限定名

java 复制代码
public class Test {
    public static void main(String[] args) {
        java.util.Date date1 = new java.util.Date();
        java.sql.Date date2 = new java.sql.Date(System.currentTimeMillis());
    }
}
9.6.4 自动导入的包

有一个包不需要手动导入,Java 会自动导入:

java.lang 包

java 复制代码
// 不需要 import,可以直接使用
String str = "Hello";
System.out.println(str);
Math.abs(-10);
Integer.parseInt("123");

java.lang 包包含了 Java 最基础的类:String、Math、System、Integer、Object 等。


9.7 包与访问修饰符的关系

还记得第 8 节我们学的访问修饰符表格吗?现在终于可以用实际代码验证了!

修饰符 本类 同包 子类(不同包) 其他包
private ✓ ✗ ✗ ✗
default(无修饰符) ✓ ✓ ✗ ✗
protected ✓ ✓ ✓ ✗
public ✓ ✓ ✓ ✓

我们来用代码验证一下"同包"和"不同包"的区别。

同一个包内的访问:

java 复制代码
// 文件:com/example/pkg1/ClassA.java
package com.example.pkg1;

public class ClassA {
    private int privateVar = 1;
    int defaultVar = 2;        // 默认权限(同包可访问)
    protected int protectedVar = 3;
    public int publicVar = 4;
}
// 文件:com/example/pkg1/ClassB.java
package com.example.pkg1;  // 注意:和 ClassA 在同一个包

public class ClassB {
    public void test() {
        ClassA a = new ClassA();
        // System.out.println(a.privateVar);    // 错误!private 不能访问
        System.out.println(a.defaultVar);       // 正确!同包可以访问 default
        System.out.println(a.protectedVar);     // 正确!同包可以访问 protected
        System.out.println(a.publicVar);        // 正确!public 都可以访问
    }
}

不同包之间的访问:

java 复制代码
// 文件:com/example/pkg2/ClassC.java
package com.example.pkg2;  // 注意:和 ClassA 在不同的包

import com.example.pkg1.ClassA;

public class ClassC {
    public void test() {
        ClassA a = new ClassA();
        // System.out.println(a.privateVar);    // 错误!private 不能访问
        // System.out.println(a.defaultVar);    // 错误!不同包不能访问 default
        // System.out.println(a.protectedVar);  // 错误!不同包且非子类不能访问 protected
        System.out.println(a.publicVar);        // 正确!只有 public 可以访问
    }
}

关键结论:

  • default 权限(无修饰符):只能在同一个包内访问,这就是为什么它也叫"包私有"
  • protected 权限:同包或者子类可以访问(子类在后续继承章节会讲)
  • 现在你应该理解为什么封装时成员变量用 private 了------它是最严格的权限

9.8 常见的包组织方式

在实际项目中,我们通常会按照一定的规则来组织包结构。主要有两种流行的方式:

9.8.1 按层分包(Layer-based)

这是企业级项目中最常见的组织方式,按照应用的分层架构来划分包:

复制代码
com.company.project/
├── entity/        实体类(数据库对应的类,也叫 model 或 domain)
├── dao/           数据访问层(Database Access Object,操作数据库)
├── service/       业务逻辑层(处理业务逻辑)
├── controller/    控制层(接收请求,调用 service)
└── util/          工具类(通用的辅助方法)

举个例子:

复制代码
com.shop/
├── entity/
│   ├── User.java
│   ├── Product.java
│   └── Order.java
├── service/
│   ├── UserService.java
│   ├── ProductService.java
│   └── OrderService.java
├── dao/
│   ├── UserDao.java
│   └── ProductDao.java
└── util/
    ├── DateUtil.java
    └── StringUtil.java

优点:

  • 职责清晰,每一层做什么一目了然
  • 大型项目中易于维护
  • 团队协作时分工明确

缺点:

  • 一个功能的相关类分散在多个包里
  • 小项目可能显得过于复杂
9.8.2 按功能分包(Feature-based)

按照业务功能模块来划分包:

复制代码
com.company.project/
├── user/          用户相关的所有类
├── product/       商品相关的所有类
├── order/         订单相关的所有类
└── common/        公共类

举个例子:

复制代码
com.shop/
├── user/
│   ├── User.java
│   ├── UserService.java
│   └── UserDao.java
├── product/
│   ├── Product.java
│   ├── ProductService.java
│   └── ProductDao.java
└── common/
    └── DateUtil.java

优点:

  • 一个功能的所有代码都在一个包里,便于查找
  • 适合中小型项目
  • 功能独立性强,便于拆分

缺点:

  • 大型项目中包会很多
  • 跨功能的通用逻辑不好放置

实际建议:

  • 小项目(<10 个类):不用太复杂,简单分 entity、service、test 就行
  • 中型项目(10-50 个类):按层分包,entity、service、util、test
  • 大型项目(>50 个类) :先按模块分,再按层分,如 com.shop.user.service

我们这次实战项目会用按层分包的方式,因为这是面试和工作中最常见的。


9.9 静态导入(回顾)

还记得我们在第 7 节学过的静态导入吗?它也是 import 的一种形式,这里简单回顾一下。

静态导入可以直接导入类的静态成员(静态变量和静态方法),使用时不需要加类名:

java 复制代码
import static java.lang.Math.*;  // 导入 Math 类的所有静态成员

public class Test {
    public static void main(String[] args) {
        // 不需要写 Math.PI 和 Math.abs
        System.out.println(PI);        // 直接使用 PI
        System.out.println(abs(-10));   // 直接使用 abs
    }
}

静态导入的语法:

java 复制代码
import static 包名.类名.静态成员;  // 导入单个静态成员
import static 包名.类名.*;         // 导入所有静态成员

使用建议:

  • 适合频繁使用的工具类方法(如 Math 的方法)
  • 不要滥用,否则代码可读性会下降(看不出方法来自哪个类)
相关推荐
ly76891 小时前
Redis 主从复制全解析:从 PSYNC 协议到复制积压缓冲区的故障切换边界
java·数据库·redis·主从复制·故障切换·psync
大圣编蚕1 小时前
C语言核心知识复习:预处理、头文件、强制类型转换、错误处理、递归与内存管理
c语言·开发语言
虎大猫猫1 小时前
Python教程——按位取反的原理
开发语言·python
Alice-YUE1 小时前
提示词工程:工业级 Prompt 写法与分层组装
java·开发语言·大模型·prompt·提示词工程·system prompt
泡茶喝茶写代码1 小时前
A股量化数据工程:从 REST 接口到策略信号(第 4 篇):行业与概念板块树构建
java·python·股票数据api·股票数据·股票数据api接口·股票api数据接口·股票量化数据api
弹简特1 小时前
【Java项目-企悦抽】17-抽奖模块02-抽奖接口实现
java·开发语言·状态模式·springboot
吴声子夜歌2 小时前
Nginx应用与运维——Nginx编译及部署(部署)
java·运维·nginx
HEJOO92 小时前
指针与结构体:C 语言进阶核心详解
c语言·开发语言·网络
乌暮2 小时前
JVM 原理与实践:从运行时数据区到线上故障排查
java·开发语言·jvm·后端·学习·面试