Java 对象与类

Java 对象与类基础

在面向对象编程(OOP)中,“对象”是现实世界实体的抽象,“类”是对象的模板或蓝图。Java 作为纯面向对象语言,一切程序设计围绕“类与对象”展开。

面向对象核心概念

对象(Object)

对象代表现实世界中可明确标识的实体(如一个学生、一个圆、一笔贷款),每个对象具有三大特征:

  • 标识(Identity):每个对象在内存中都有唯一地址,用于区分不同对象。
  • 状态(State):也称属性/特征,由数据域(成员变量)描述,如圆的radius(半径)、学生的name(姓名)。
  • 行为(Behavior):也称动作,由方法描述,如圆的getArea()(计算面积)、学生的study()(学习)。

类(Class)

类是对象的“模板”,定义了对象的数据域结构方法行为。通过类可以创建多个实例(对象),实例的类型相同但状态可不同。

  • 例:“学生”是类,“张三”“李四”是该类的实例(对象)。
  • 关系:类 → 实例化 → 对象(new关键字实现实例化)。

类的定义与实现

以“圆(Circle)”类为例,完整展示类的结构(包含成员变量、构造方法、成员方法)。

类的基本结构(UML)

下图为 Circle 类的类图,描述了类的属性和方法:
image

Circle 类代码实现

package com.oop;

/**
 * 圆类:描述圆的属性和行为
 * @author Jing61
 */
public class Circle {
    // 1. 成员变量(属性/状态):描述圆的特征
    public double radius; // 半径

    // 2. 构造方法:对象创建时调用,用于初始化对象
    // 无参构造方法:若未显式定义,编译器会自动添加默认无参构造
    public Circle() {
        System.out.println("构造方法,在初始化时调用");
    }

    // 3. 成员方法(行为):描述圆的操作
    // 获取半径(访问器方法:getter)
    public double getRadius() {
        return radius;
    }

    // 设置半径(修改器方法:setter)
    public void setRadius(double radius) {
        this.radius = radius; // this 指代当前对象,用于区分成员变量和参数
    }

    // 计算面积
    public double getArea() {
        return radius * radius * Math.PI; // Math.PI 是 Java 内置常量(π)
    }

    // 计算周长
    public double getPerimeter() {
        return 2 * radius * Math.PI;
    }
}

构造方法(Constructor)

构造方法是类的特殊方法,用于对象的初始化,在使用new关键字创建对象时自动调用。

构造方法的核心规则

  1. 方法名必须与类名完全相同(大小写敏感)。
  2. 无返回值类型(连void都不能写)。
  3. 可重载:一个类可以有多个构造方法,只要参数列表(个数、类型、顺序)不同。
  4. 默认构造:若类中未显式定义任何构造方法,编译器会自动添加一个无参、方法体为空的默认构造;若已定义构造方法,默认构造会被覆盖。

构造方法的重载示例

为 Circle 类添加“带参数的构造方法”,支持创建对象时直接设置半径:

public class Circle {
    public double radius;

    // 无参构造
    public Circle() {
        System.out.println("无参构造方法调用");
        this.radius = 1.0; // 默认半径为 1.0
    }

    // 带参数构造(重载):创建对象时指定半径
    public Circle(double radius) {
        System.out.println("带参数构造方法调用");
        this.radius = radius; // this.radius 指成员变量,radius 指参数
    }

    // 成员方法(省略 getter/setter、getArea 等)
}

对象的创建与使用

通过new关键字创建对象,并通过“对象名.成员”的方式访问成员变量和调用方法。

测试类实现(CircleTest)

package com.oop;

/**
 * Circle 类的测试类
 * @Author Jing61
 */
public class CircleTest {
    public static void main(String[] args) {
        // 1. 创建对象(调用构造方法)
        Circle c1 = new Circle(); // 调用无参构造,radius 默认 1.0
        Circle c2 = new Circle(25.0); // 调用带参构造,radius 设为 25.0
        Circle c3 = new Circle(125.0); // 调用带参构造,radius 设为 125.0

        // 2. 访问成员变量和调用方法
        System.out.println("=== c1 信息 ===");
        System.out.println("半径为:" + c1.getRadius());
        System.out.println("面积:" + c1.getArea());
        System.out.println("周长:" + c1.getPerimeter());

        System.out.println("\n=== c2 信息 ===");
        System.out.println("半径为:" + c2.getRadius());
        System.out.println("面积:" + c2.getArea());
        System.out.println("周长:" + c2.getPerimeter());

        System.out.println("\n=== c3 信息 ===");
        System.out.println("半径为:" + c3.getRadius());
        System.out.println("面积:" + c3.getArea());
        System.out.println("周长:" + c3.getPerimeter());
    }
}

运行结果

image

关键说明

  • 每个对象的成员变量是独立的(如 c1、c2、c3 的radius互不影响)。
  • new关键字的作用:① 为对象在堆内存中分配空间;② 调用构造方法初始化对象;③ 返回对象的引用(地址)给变量(如 c1)。

成员变量的默认值与引用类型

成员变量的默认值

若成员变量未显式初始化,Java 会自动赋予默认值,规则如下:

数据类型分类 默认值 示例
数值型(byte/int/long 等) 0(浮点型为 0.0) int age → 0
字符型(char) '\u0000'(空字符) char gender'\u0000'
布尔型(boolean) false boolean isPass → false
引用类型(String/类等) null(无引用对象) String name → null

注意:局部变量(方法内定义的变量)无默认值,必须显式初始化才能使用,否则编译报错。例如:

public static void main(String[] args) {
    int x; // 局部变量未初始化
    System.out.println(x); // 编译错误:变量 x 可能尚未初始化
}

引用类型成员变量(以 Student 类为例)

引用类型成员变量存储的是对象的“引用(地址)”,默认值为null(表示未指向任何对象)。

class Student {
    String name; // 引用类型,默认 null
    int age; // 数值型,默认 0
    boolean isScienceMajor; // 布尔型,默认 false
    char gender; // 字符型,默认 '\u0000'
}
  • name未赋值,直接调用name.length()会抛出NullPointerException(空指针异常),需先通过name = "张三"为其赋值。

基本类型变量与引用类型变量的区别

两种变量在内存存储和赋值逻辑上存在本质差异。

内存存储差异

变量类型 内存存储内容 存储位置
基本类型 实际值(如 10、3.14) 栈内存(Stack)
引用对象本身 对象的成员变量(状态) 堆内存(Heap)

赋值逻辑差异

基本类型赋值

赋值时直接复制“实际值”,两个变量相互独立。

int i = 1, j = 2;
i = j; // j 的值(2)复制给 i,i=2,j 仍为 2

引用类型赋值

赋值时复制“对象引用”,两个变量指向同一个对象(修改一个会影响另一个)。

// 创建 Date 对象,birthday 存储对象的堆地址
Date birthday = new Date();
Date deadline = birthday; // 复制 birthday 的引用,deadline 与 birthday 指向同一个对象

deadline.setTime(0); // 修改 deadline 指向的对象,birthday 指向的对象也会变化

示意图

image

Java 内存管理优势

  • 所有对象存储在堆中,引用变量存储在栈中。
  • 无需手动管理内存:Java 的垃圾回收器(GC) 会自动回收无引用指向的对象,避免内存泄漏。
  • 空指针安全:若引用变量为null(未指向对象),调用其方法会抛出NullPointerException,而非随机错误。

Java 预定义类的使用

Java 提供了大量预定义类(如DateLocalDateRandom),可直接用于开发,无需重复造轮子。

Date 类(处理时间戳与日期)

java.util.Date类用于表示“时间点”(距离 1970-01-01 00:00:00 GMT 的毫秒数),常与SimpleDateFormat配合进行日期格式化。

代码示例

package com.oop;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Author Jing61
 */
public class DateDeom {
    public static void main(String[] args) throws ParseException {
        // 获取当前时间戳
        System.out.println(System.currentTimeMillis());

        // 为当前时间创建一个Date对象
        Date date = new Date();
        System.out.println(date); // 调用 date.toString() 方法

        // date.setTime(0); // 为date设置一个新的流逝时间
        /*
         * 对Date进行格式化(字符串)
         * pattern:
         *  y: year
         *  M: month
         *  d: day
         *  H: hour 24 小时的时间
         *  h: hour 12 小时的时间
         *  m: minute
         *  s: second
         */
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        // 将date对象转化为指定格式的字符串
        System.out.println(sdf.format(date));

        // 将指定格式的字符串转化为date对象
        String pattern = "2025-10-31 13:12:50";
        Date date1 = sdf.parse(pattern); // 这儿需要做异常处理
        System.out.println(date1);
    }
}

image

关键说明

  • SimpleDateFormat的模式字符:y(年)、M(月)、d(日)、H(24小时制)、h(12小时制)、m(分)、s(秒)。
  • Date类的局限性:不便于处理“日历日期”(如“2025年10月31日”),Java 8 后推荐使用LocalDate

LocalDate 类(处理日历日期)

java.time.LocalDate(Java 8 引入)专门用于表示“日历日期”(无时间部分),支持日期计算(如加天数、获取年/月/日)。

核心方法与示例

方法 功能描述 示例
LocalDate.now() 获取当前日期 LocalDate today = LocalDate.now();
LocalDate.of(y, M, d) 创建指定日期的对象 LocalDate birthday = LocalDate.of(2008, 10, 20);
getYear() 获取年份 int year = birthday.getYear(); // 2008
getMonthValue() 获取月份(1-12) int month = birthday.getMonthValue(); // 10
plusDays(n) 增加 n 天,返回新的 LocalDate 对象 LocalDate later = birthday.plusDays(1000);

代码示例

package com.oop;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.time.LocalDate;

/**
 * @Author Jing61
 */
public class LocalDateDemo {
    public static void main(String[] args) {
        // 1. 获取当前日期
        LocalDate today = LocalDate.now();
        System.out.println("当前日期:" + today); // 输出:2025-10-31(示例值)

        // 2. 创建指定日期
        LocalDate nationalDay = LocalDate.of(2025, 10, 1);
        System.out.println("国庆节:" + nationalDay); // 输出:2025-10-01

        // 3. 日期计算(增加 100 天)
        LocalDate future = nationalDay.plusDays(100);
        System.out.println("100天后:" + future); // 输出:2026-01-09(示例值)

        // 4. 获取日期字段
        System.out.println("年份:" + future.getYear()); // 2026
        System.out.println("月份:" + future.getMonthValue()); // 1
    }
}

Random 类(生成随机数)

java.util.Random类用于生成多种类型的随机数(int、long、double 等),支持指定种子(种子相同则随机序列相同,便于测试)。

核心方法与示例

方法 功能描述
Random() 无参构造:以当前时间为种子
Random(long seed) 带参构造:以指定 seed 为种子
nextInt() 返回随机 int 值(范围:int 最小值 ~ 最大值)
nextInt(n) 返回随机 int 值(范围:0 ~ n-1,不含 n)
nextDouble() 返回随机 double 值(范围:0.0 ~ 1.0,不含 1.0)

代码示例

package com.oop;

import java.util.Random;

/**
 * @Author Jing61
 */
public class RandomDemo {
    /**
     * 可以使用Math.random()获取一个0.0到1.0(不含)之间的随机double型值
     * 使用Random产生随机的int、long、float、double、boolean值
     *
     * Random():以当前时间作为种子创建Random对象
     * Random(seed:long):以特定值seed作为种子创建随机对象
     *
     * nextInt():int 返回随机的int值
     * nextInt(n:int):int 返回一个0到n(不含)之间的随机整数
     * nextLong():long
     * nextFloat():float
     * nextDouble():double
     * nextBoolean():boolean
     */
    public static void main(String[] args) {
        Random random = new Random();
        System.out.println("随机的int值:" + random.nextInt());
        System.out.println("随机的int值[0--100):" + random.nextInt(100));
        System.out.println("随机的long值:" + random.nextLong());
        System.out.println("随机的float值:" + random.nextFloat());
        System.out.println("随机的double值:" + random.nextDouble());
        System.out.println("随机的boolean值:" + random.nextBoolean());

        /*
         * 如果种子数相同,产生的随机序列也是相同
         */
        Random random1 = new Random(10);
        for(int i = 0; i < 10; i++)
            System.out.print(random1.nextInt(100) + " ");
        System.out.println();
        Random random2 = new Random(10);
        for(int i = 0; i < 10; i++)
            System.out.print(random2.nextInt(100) + " ");
    }
}

static 关键字(静态成员)

static关键字用于修饰成员变量方法代码块,表示“属于类,而非实例”,被所有实例共享。

静态变量(类变量)

静态变量存储在“类的公共内存区域”,所有实例共享其值;实例变量存储在每个实例的堆内存中,相互独立。

示例(Student 类)

public class Student {
    int id; // 实例变量:每个学生的 id 不同
    static int nextId; // 静态变量:所有学生共享,用于生成下一个 id
    String name; // 实例变量:每个学生的姓名不同
}

访问方式

  • 推荐:类名.静态变量(如Student.nextId)。
  • 不推荐:对象.静态变量(如peppa.nextId,易混淆“静态变量属于类”的本质)。

示例代码

public class StudentTest {
    public static void main(String[] args) {
        Student.nextId = 1; // 初始化静态变量

        Student peppa = new Student();
        peppa.id = Student.nextId++; // peppa.id = 1,nextId 变为 2
        peppa.name = "Peppa";

        Student suzy = new Student();
        suzy.id = Student.nextId++; // suzy.id = 2,nextId 变为 3
        suzy.name = "Suzy";

        System.out.println("Peppa id:" + peppa.id); // 1
        System.out.println("Suzy id:" + suzy.id); // 2
        System.out.println("下一个 id:" + Student.nextId); // 3
    }
}

静态常量

静态变量若被final修饰,即为“静态常量”,表示“全局不可变的值”,推荐用public static final修饰。

  • 例:Math.PI(π 值)、Integer.MAX_VALUE(int 最大值)。

示例

public class MathUtils {
    // 静态常量:圆周率(全局不可变)
    public static final double PI = 3.1415926538979323846;
    // 静态常量:地球半径(单位:千米)
    public static final double EARTH_RADIUS = 6371.0;
}

访问方式

直接通过类名访问:MathUtils.PI

静态方法(类方法)

静态方法不依赖实例,无需创建对象即可调用,只能访问静态成员(静态变量、静态方法),不能访问实例成员(实例变量、实例方法)。

示例(Student 类的静态方法)

public class Student {
    private static int nextId = 1;

    // 静态方法:获取下一个 id
    public static int getNextId() {
        return nextId; // 可访问静态变量 nextId
    }

    // 实例方法:必须实例化才能调用
    public void study() {
        System.out.println("学习中...");
    }
}

调用方式

public class StaticMethodTest {
    public static void main(String[] args) {
        // 直接通过类名调用静态方法,无需创建对象
        int nextId = Student.getNextId();
        System.out.println("下一个 id:" + nextId); // 1

        // 错误:未实例化不能调用实例方法
        // Student.study(); // 编译报错
    }
}

静态方法的适用场景

  1. 方法不需要访问实例状态(如Math.pow(a, b))。
  2. 方法仅需访问类的静态成员(如Student.getNextId())。

初始化块与静态代码块

初始化块

初始化块用于初始化实例变量,在每个对象创建时执行(执行顺序:初始化块 → 构造方法主体)。

示例

package com.oop;

/**
 * 含初始化块的 Student 类
 * @Author Jing61
 */
public class Student {
    private int id;
    private static int nextId = 1;
    private String name;

    // 初始化块:每个对象创建时执行,初始化 id
    {
        id = nextId++; // 为当前学生分配 id,并更新 nextId
    }

    // 构造方法
    public Student() {}
    public Student(String name) {
        this.name = name; // 构造方法主体在实例代码块后执行
    }

    // getter/setter(省略)
}

说明

  • 初始化块不是必需的,通常可将初始化逻辑直接写入构造方法,提高代码可读性。
  • 若多个构造方法有相同的初始化逻辑,可将其提取到初始化块,避免代码冗余。

静态代码块

静态代码块用于初始化静态变量,在类第一次加载时执行(仅执行一次),执行顺序优于实例代码块和构造方法。

示例(Student 类的静态代码块)

package com.oop;

import java.util.Random;

/**
 * 含静态代码块的 Student 类
 * @Author Jing61
 */
public class Student {
    private int id;
    private static int nextId;
    private String name;

    // 静态代码块:初始化静态变量 nextId(类加载时执行)
    static {
        Random generator = new Random();
        nextId = generator.nextInt(10000); // 生成 0~9999 的随机初始 id
    }

    // 初始化块:初始化实例变量 id(每个对象创建时执行)
    {
        id = nextId++; // 为每个学生分配唯一 id
    }

    // 构造方法
    public Student() {}
    public Student(String name) {
        this.name = name;
    }

    // getter/setter(省略)
}

说明

在类第一次加载的时候,将会进行静态字段的初始化。与实例字段一样,除非将静态字段显式地设置成其他值,否则默认的初始值是 0、false 或 null。

执行顺序

所有的静态字段初始化方法以及静态初始化块都将依照类声明中出现的顺序执行:静态代码块 > main > 构造代码块 > 构造器 > 普通代码块。

可见性修饰符(访问控制)

可见性修饰符用于控制类、成员(变量/方法)的访问范围,保障数据安全和代码封装。

修饰符 访问范围 适用对象
public 所有类(跨包、跨类) 类、成员
protected 同一包内的类 + 不同包的子类 成员(不适用类)
private 仅当前类内 成员(不适用类)

常见使用场景

  1. 数据域封装:用private修饰成员变量,通过public的 getter/setter 方法访问和修改,防止数据被直接篡改。
    示例(Student 类的封装):

    public class Student {
        // 私有成员变量(外部无法直接访问)
        private int id;
        private static int nextId;
        private String name;
    
        // getter 方法:获取 id(只读)
        public int getId() {
            return id;
        }
    
        // setter 方法:设置 name(可控制赋值逻辑,如非空校验)
        public void setName(String name) {
            if (name == null || name.trim().isEmpty()) {
                throw new IllegalArgumentException("姓名不能为空");
            }
            this.name = name;
        }
    
        // 其他代码(省略)
    }
    
  2. 私有构造方法:若类的所有方法都是静态的(如Math类),可将构造方法设为private,防止创建对象。
    示例(Math 类的私有构造):

    public class Math {
        // 私有构造方法:外部无法通过 new Math() 创建对象
        private Math() {}
    
        // 静态方法(如 pow、sqrt 等)
        public static double pow(double a, double b) {
            // 计算逻辑
        }
        .....
    }
    

向方法传递对象参数

Java 只有一种参数传递方式:值传递(pass-by-value),但基本类型和引用类型的传递逻辑不同。

基本类型参数传递

传递“实际值”的副本,方法内修改参数值不影响外部变量。

public static void increment(int x) {
    x++; // 方法内修改的是副本,外部变量不变
}

public static void main(String[] args) {
    int a = 10;
    increment(a);
    System.out.println(a); // 输出:10(外部变量未变)
}

引用类型参数传递

传递“对象引用”的副本,方法内通过引用修改对象的成员变量,会影响外部对象(因为副本和原引用指向同一个对象)。

示例(传递 Circle 对象)

package com.oop;

public class CircleTest {
    public static void printArea(Circle circle, int times) {
        System.out.println("Radis\tArea");
        for(int i = 1; i <= times; i++) {
            circle.setRadius(i);
            System.out.println(circle.getRadius() + "\t" + String.format("%.5f", circle.getArea()));
        }
    }

    public static void main(String[] args) {
        Circle circle = new Circle();
        int times = 5;
        printArea(circle, times);
        System.out.println(circle.getRadius() + "\t" + String.format("%.5f", circle.getArea()));// 5.0	78.53982
    }
}

内存示意图

image

对象数组

对象数组本质是“引用变量的数组”,数组中的每个元素存储的是对象的引用(而非对象本身)。

示例(创建 Student 对象数组)

public class ObjectArrayDemo {
    public static void main(String[] args) {
        // 1. 声明并初始化 Student 数组(长度为 3)
        Student[] students = new Student[3];

        // 2. 为数组元素赋值(每个元素需指向一个 Student 对象)
        students[0] = new Student("张三");
        students[1] = new Student("李四");
        students[2] = new Student("王五");

        // 3. 遍历数组,访问对象的成员
        for (int i = 0; i < students.length; i++) {
            System.out.printf("学生%d:姓名=%s,id=%d%n", 
                i+1, students[i].getName(), students[i].getId());
        }
    }
}

关键说明

  • 数组初始化时,仅分配“引用变量的存储空间”,每个元素默认值为null,需手动通过new创建对象并赋值,否则调用students[i].getName()会抛出NullPointerException

不可变对象和类

不可变对象是指“创建后其状态(成员变量)无法修改”的对象,对应的类称为不可变类(如String类)。

不可变类的设计规则

  1. 所有成员变量用private修饰(禁止外部直接访问)。
  2. 无修改器方法(无 setter 方法)。
  3. 访问器方法(getter)不返回可变成员变量的引用(避免外部通过引用修改内部状态)。
  4. 若成员变量是引用类型,需返回其副本(如clone())。

示例(不可变类 Employee)

package com.oop;

import java.util.Date;

/**
 * @Author Jing61
 */
public class Employee {
    private static int nextId;
    private int id;
    private String name;
    private Date hireDate;
    public Employee(String name) {
        this.id = nextId;
        nextId++;
        this.name = name;
        this.hireDate = new Date();
    }
    public int getId() {
        return id;
    }
    public String getName() {
        return name;
    }
    public Date getHireDate() {
        //返回的是Date对象的一个引用。通过这个引用可以改变hireDate的值
        //return hireDate;
        //可以修改为:
        return (Date)hireDate.clone();
    }
}

成员变量 vs 局部变量

对象的属性就是成员变量。局部变量的声明和使用都在一个方法的内部。

  • 共同点:
    1. 都是变量,他们的定义形式相同:类型 变量名 = 初始化值。
    2. 都有作用域:作用域是在一对大括号内。
  • 不同点:
    1. 内存中存放的位置不同:成员变量存放在堆空间内。局部变量存放在栈空间内。
    2. 声明的位置不同(作用域不同):成员变量声明在类的内部,方法的外部,作用整个类;局部变量声明在方法的内部,从它声明的地方开始到包含它最近的块结束。
    3. 初始化值不同:成员变量可以不赋初值,其默认值按照其数据类型来定;局部变量必须显式地赋初值。
    4. 权限修饰符不同:成员变量的权限修饰符有四个:public (default) protected private;局部变量没有权限修饰符,其访问权限依据其所在的方法而定(与方法的访问权限相同)。

同名隐藏

如果一个局部变量和一个成员变量具有相同的名字,那么局部变量优先,而同名的类变量将被隐藏。

public class Demo{
     private int x = 0;
     private int y = 0;

     public void test(){
          int x = 100;
          System.out.println("x = " + x); // 100
          System.out.println("y = " + y);// 0
     }
}

用 var 声明局部变量(Java 10+)

var关键字用于简化局部变量声明,编译器可从初始值推导出变量类型,仅适用于方法内的局部变量(参数、成员变量不可用)。

示例

Circle circle = new Circle(10);
//只需要写以下代码:
var circle = new Circle(10);

注意事项

  • var不能用于成员变量、方法参数、返回值类型。
  • 初始值必须存在(如var x;编译报错,需var x = 10;)。

this 引用

this是关键字,指代“当前对象”,主要用于区分成员变量与局部变量、调用同类的其他构造方法。

场景 1:区分成员变量与局部变量

当方法参数与成员变量同名时,用this.成员变量访问成员变量。

public class Circle {
    private double radius;

    // 参数 radius 与成员变量同名
    public void setRadius(double radius) {
        this.radius = radius; // this.radius:成员变量;radius:参数
    }
}

场景 2:调用同类的其他构造方法

this(参数列表)在一个构造方法中调用另一个构造方法,必须放在构造方法的第一行

public class Circle {
    private double radius;

    // 无参构造:调用带参构造,默认半径 1.0
    public Circle() {
        this(1.0); // 调用 Circle(double radius),必须在第一行
    }

    // 带参构造
    public Circle(double radius) {
        this.radius = radius;
    }
}

场景 3:省略 this(无冲突时)

若方法中无同名局部变量,this可省略(编译器默认访问成员变量)。

public class Circle {
    private double radius;

    public double getArea() {
        // 省略 this,等价于 return Math.PI * this.radius * this.radius;
        return Math.PI * radius * radius;
    }
}
posted @ 2025-10-31 14:34  Jing61  阅读(14)  评论(0)    收藏  举报