写在前面:这是JavaSE系列的第10篇,也是面向对象篇的最后一篇。今天我们学习Java中最常用的几个类——Object、Math、日期类,以及包装类。这些是Java开发中每天都会用到的,必须掌握。

在这里插入图片描述


一、Object类:所有类的父类

1.1 为什么Object类如此重要?

在实际开发中,你是否遇到过这样的场景?

// 场景:打印对象信息,输出的是乱码
Person p = new Person("张三", 20);
System.out.println(p);  // 输出:Person@15db9742

// 场景:比较两个对象,明明内容一样却返回false
Person p1 = new Person("张三", 20);
Person p2 = new Person("张三", 20);
System.out.println(p1 == p2);  // false

这些问题都源于Object类的方法。Java中所有类都直接或间接继承Object类,理解它是写出高质量代码的基础。

class Person { }  // 相当于 class Person extends Object { }
class Student extends Person { }  // Student间接继承Object

1.2 Object的常用方法

方法 说明
toString() 返回对象的字符串表示
equals(Object obj) 比较两个对象是否相等
hashCode() 返回对象的哈希码值
getClass() 返回对象的运行时类型
clone() 创建对象的副本
finalize() 垃圾回收时调用(已废弃)

1.3 toString方法

踩坑提醒:很多新手直接打印对象,看到Person@15db9742这样的输出一脸懵,其实这是Object默认的toString实现。

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

public class Test {
    public static void main(String[] args) {
        Person p = new Person("张三", 20);
        
        // 不重写toString,默认输出:类名@哈希码
        System.out.println(p.toString());
        // 输出:Person@15db9742
        
        // 快捷键:Alt + Insert → toString()
        // 重写后输出:Person{name='张三', age=20}
    }
}

经验之谈:在实际项目中,我习惯为每个实体类都重写toString,这样在日志输出和调试时能快速定位问题。IDEA的自动生成快捷键Alt + Insert可以一键生成,非常方便。

IDEA自动生成toString

@Override
public String toString() {
    return "Person{" +
            "name='" + name + '\'' +
            ", age=" + age +
            '}';
}

1.4 equals方法

踩坑提醒==equals的区别是面试必考点,也是新手最容易混淆的地方。

class Person {
    String name;
    int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // IDE自动生成equals(Alt + Insert → equals and hashCode)
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }
}

public class Test {
    public static void main(String[] args) {
        Person p1 = new Person("张三", 20);
        Person p2 = new Person("张三", 20);
        
        System.out.println(p1 == p2);        // false(==比较地址)
        System.out.println(p1.equals(p2));   // true(比较内容)
    }
}

经验之谈:在集合操作中(如HashMap、HashSet),equals和hashCode必须同时重写,这是Java的约定。如果只重写equals而不重写hashCode,会导致集合行为异常。

1.5 hashCode方法

为什么equals和hashCode要一起重写?

在HashMap中,key的查找过程是:先通过hashCode定位桶,再通过equals比较。如果两个对象equals相等但hashCode不同,会导致同一个key存进两个不同的桶,违背HashMap的设计。

// hashCode的约定:
// 1. 同一个对象多次调用hashCode,返回值必须相同
// 2. 两个对象equals返回true,hashCode必须相同
// 3. 两个对象equals返回false,hashCode可以相同(但最好不同)

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person person = (Person) o;
    return age == person.age && Objects.equals(name, person.name);
}

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

踩坑提醒:曾经遇到过一个Bug,Person对象作为HashMap的key,存进去后却取不出来。排查半天发现只重写了equals没重写hashCode,导致hashCode不一致,定位到了错误的桶。


二、Math类:数学工具

2.1 常用方法

// 取整
Math.ceil(3.14);   // 4.0,向上取整
Math.floor(3.14);  // 3.0,向下取整
Math.round(3.14);  // 3,四舍五入
Math.round(3.54);  // 4

// 绝对值
Math.abs(-5);       // 5
Math.abs(-3.14);   // 3.14

// 最大最小值
Math.max(10, 20);  // 20
Math.min(10, 20);  // 10

// 幂运算
Math.pow(2, 3);    // 8.0,2的3次方
Math.sqrt(16);     // 4.0,平方根
Math.cbrt(27);     // 3.0,立方根

// 对数
Math.log(2.71828);  // 1.0,自然对数
Math.log10(100);    // 2.0,底数为10的对数

// 三角函数
Math.sin(Math.PI / 2);  // 1.0
Math.cos(0);            // 1.0
Math.tan(Math.PI / 4);  // 1.0

2.2 随机数

实际应用场景:验证码生成、抽奖系统、测试数据构造都需要用到随机数。

// 1. Math.random():返回[0, 1)之间的随机数
double random = Math.random();  // 0.0 ~ 0.999...

// 生成指定范围的随机整数
int num = (int)(Math.random() * 100);  // 0 ~ 99
int num2 = (int)(Math.random() * 100) + 1;  // 1 ~ 100

// 2. Random类(更灵活)
import java.util.Random;

Random r = new Random();  // 默认种子(当前时间)
Random r2 = new Random(100);  // 指定种子(种子相同,产生的随机数序列相同)

r.nextInt();         // 任意int值
r.nextInt(100);      // [0, 100)之间的int值
r.nextDouble();       // [0.0, 1.0)之间的double值
r.nextBoolean();      // true或false

经验之谈:在需要可重复随机序列的场景(如游戏存档、测试用例),使用指定种子的Random非常重要。例如游戏地图生成,相同种子生成相同的地图,便于玩家分享。


三、日期时间类

3.1 Date类

import java.util.Date;

// 当前时间
Date now = new Date();
System.out.println(now);  // Thu Jan 15 10:30:45 CST 2024

// 毫秒值转Date
long time = 1704067200000L;
Date date = new Date(time);

// Date转毫秒值
long timestamp = now.getTime();

// 日期比较
Date d1 = new Date();
Date d2 = new Date();
d1.before(d2);  // d1是否在d2之前
d1.after(d2);   // d1是否在d2之后
d1.compareTo(d2);  // 返回负数、0、正数

3.2 SimpleDateFormat:日期格式化

踩坑提醒:SimpleDateFormat不是线程安全的!在高并发场景下,多个线程共享同一个SimpleDateFormat实例会导致日期解析错误。

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

Date now = new Date();

// 格式化:Date → String
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = sdf.format(now);
System.out.println(dateStr);  // 2024-01-15 10:30:45

String dateStr2 = new SimpleDateFormat("yyyy年MM月dd日 E").format(now);
System.out.println(dateStr2);  // 2024年01月15日 周一

// 解析:String → Date
String str = "2024-01-15";
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf2.parse(str);

解决方案

  1. 每次使用都new一个SimpleDateFormat(简单但性能差)
  2. 使用ThreadLocal为每个线程创建一个实例
  3. 升级到Java 8,使用DateTimeFormatter(线程安全,推荐)

格式化符号

符号 含义 示例
yyyy 2024
MM 01~12
dd 01~31
HH 小时(24) 00~23
hh 小时(12) 01~12
mm 分钟 00~59
ss 00~59
E 星期 周一

3.3 LocalDateTime(Java 8+,推荐)

为什么推荐LocalDateTime?

  1. 线程安全:DateTimeFormatter是线程安全的,可以在多线程环境下共享
  2. 不可变对象:所有修改操作都返回新对象,不会出现意外修改
  3. API设计清晰:日期和时间分开处理,语义明确
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

// LocalDate:只有日期
LocalDate date = LocalDate.now();
System.out.println(date);  // 2024-01-15

// LocalTime:只有时间
LocalTime time = LocalTime.now();
System.out.println(time);  // 10:30:45.123456

// LocalDateTime:日期+时间(常用)
LocalDateTime ldt = LocalDateTime.now();
System.out.println(ldt);  // 2024-01-15T10:30:45.123456

// 格式化
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
String str = ldt.format(dtf);
System.out.println(str);  // 2024年01月15日 10:30:45

// 解析
LocalDateTime ldt2 = LocalDateTime.parse("2024-01-15 10:30:45",
    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

// 计算
LocalDateTime future = ldt.plusDays(7);   // 7天后
LocalDateTime past = ldt.minusHours(2);   // 2小时前

// 日期比较
LocalDate d1 = LocalDate.of(2024, 1, 1);
LocalDate d2 = LocalDate.of(2024, 12, 31);
System.out.println(d1.isBefore(d2));  // true
System.out.println(d2.isAfter(d1));   // true

经验之谈:在新项目中,我坚决使用LocalDateTime替代Date。曾经维护过一个老项目,里面Date和LocalDateTime混用,转换起来非常痛苦。建议新项目统一使用Java 8时间API。


四、包装类:基本类型的"对象包装"

4.1 为什么要包装类?

// 基本类型不是对象,不能用于泛型
// List<int> list = new ArrayList<>();  // 错误!

// 包装类是对象,可以用于泛型
List<Integer> list = new ArrayList<>();  // 正确!

4.2 基本类型对应的包装类

基本类型 包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

4.3 装箱和拆箱

// 手动装箱
Integer a = Integer.valueOf(10);  // 方式1
Integer b = new Integer(10);      // 方式2(已废弃)

// 手动拆箱
int c = a.intValue();  // 方式1

// 自动装箱(编译时自动转换为valueOf)
Integer d = 10;  // 自动装箱:Integer.valueOf(10)

// 自动拆箱
int e = d;  // 自动拆箱:d.intValue()

// 实际应用
int sum = 0;
for (Integer i : list) {  // 自动拆箱
    sum += i;  // 自动拆箱
}

4.4 缓存机制

面试高频考点:Integer缓存是面试官最爱问的知识点之一,务必理解透彻。

// Integer缓存:-128 ~ 127
Integer a = 127;
Integer b = 127;
System.out.println(a == b);  // true(缓存范围内)

Integer c = 128;
Integer d = 128;
System.out.println(c == d);  // false(超出缓存范围)

// Byte、Short、Long也有缓存
// Character缓存:0 ~ 127
// Boolean:TRUE和FALSE两个常量

原理揭秘:Integer.valueOf()方法内部使用了IntegerCache,-128到127之间的整数会被缓存起来,多次获取返回同一个对象。超出这个范围就会new一个新的Integer对象。

踩坑提醒:比较包装类一定要用equals,不要用==。虽然缓存范围内的==可能返回true,但这是不可靠的实现细节,不同JDK版本可能不同。

4.5 字符串转换

// String → 基本类型
int i = Integer.parseInt("100");      // 100
double d = Double.parseDouble("3.14"); // 3.14
boolean b = Boolean.parseBoolean("true"); // true

// String → 包装类
Integer i = Integer.valueOf("100");

// 基本类型 → String
String s1 = String.valueOf(100);       // "100"
String s2 = Integer.toString(100);    // "100"
String s3 = 100 + "";                 // "100"

// 包装类 → String
String s4 = new Integer(100).toString(); // "100"

4.6 注意事项

踩坑提醒:这三个坑我在实际开发中都踩过,血泪教训!

// ⚠️ 自动拆箱可能报NullPointerException
Integer i = null;
int j = i;  // 自动拆箱:i.intValue() → NullPointerException!

// ⚠️ 包装类的==比较
Integer a = 100;
Integer b = 100;
System.out.println(a == b);  // true(缓存范围内)

Integer c = 200;
Integer d = 200;
System.out.println(c == d);  // false(超出缓存)

// ⚠️ 建议:比较包装类用equals
System.out.println(c.equals(d));  // true

经验之谈

  1. 数据库查询返回的Integer可能为null,赋值给int前一定要判空
  2. 三目运算符中如果有一个操作数是基本类型,另一个会自动拆箱,也容易NPE
  3. 团队规范建议:所有包装类比较统一使用equals,避免==的迷惑行为

五、System类

5.1 常用方法

性能测试必备System.currentTimeMillis()System.nanoTime()是性能测试的常用工具。

// 1. System.currentTimeMillis():获取当前时间毫秒值
long start = System.currentTimeMillis();
// 执行代码...
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "ms");

// 2. System.arraycopy():数组拷贝
int[] src = {1, 2, 3, 4, 5};
int[] dest = new int[5];
System.arraycopy(src, 0, dest, 0, 5);

// 3. System.gc():建议垃圾回收器回收(不保证立即执行)
System.gc();

// 4. System.exit(0):退出JVM
// System.exit(0);  // 0表示正常退出,非0表示异常退出

// 5. System.getProperty():获取系统属性
System.out.println(System.getProperty("java.version"));    // Java版本
System.out.println(System.getProperty("os.name"));       // 操作系统
System.out.println(System.getProperty("user.dir"));      // 当前目录

经验之谈System.arraycopy()是native方法,比手动for循环拷贝数组快得多。在需要高性能数组操作的场景下优先使用。


六、String类的高级操作

6.1 String的split方法

String s = "Java,Python,JavaScript";

// 按逗号分割
String[] arr = s.split(",");  // ["Java", "Python", "JavaScript"]

// 限制分割次数
String[] arr2 = s.split(",", 2);  // ["Java", "Python,JavaScript"]

// 按正则分割
String s2 = "Java1Python2JavaScript3Go";
String[] arr3 = s2.split("\\d");  // 按数字分割

6.2 String的matches方法

// 判断是否匹配正则
String phone = "13812345678";
boolean valid = phone.matches("1[3-9]\\d{9}");
System.out.println(valid);  // true

String email = "test@example.com";
boolean valid2 = email.matches("\\w+@\\w+\\.\\w+");
System.out.println(valid2);  // true

6.3 String的replaceAll

String s = "Java123Python456";

// 替换所有数字为#
String s2 = s.replaceAll("\\d", "#");  // Java###Python###

// 删除所有数字
String s3 = s.replaceAll("\\d", "");  // JavaPython

// 替换字符串(不支持正则)
String s4 = s.replace("Java", "C++");  // C++123Python456

七、面试高频考点

考点1:Integer缓存机制

Integer a = 127;
Integer b = 127;
System.out.println(a == b);  // true

Integer c = 128;
Integer d = 128;
System.out.println(c == d);  // false

延伸问题:缓存范围可以调整吗?
答案:可以通过-XX:AutoBoxCacheMax=<size>参数调整Integer缓存上限。

考点2:==和equals的区别

// 基本类型:==比较值
// 引用类型:==比较地址,equals比较内容(需要重写)

追问:String的equals是怎么实现的?
答案:String重写了equals,先比较地址,再比较长度,最后逐个字符比较。

考点3:自动拆箱的NPE问题

Integer i = null;
int j = i;  // NullPointerException!

实际场景:从数据库查询返回Integer,直接赋值给int类型字段时容易触发。

考点4:Date和LocalDateTime

// Date:旧API,线程不安全
// LocalDateTime:Java 8+,线程安全,推荐使用

延伸:SimpleDateFormat为什么线程不安全?
答案:SimpleDateFormat内部使用Calendar对象存储中间状态,多线程并发修改会导致数据混乱。


八、总结

今天我们学习了:

  • ✅ Object类的常用方法
  • ✅ Math类的数学运算
  • ✅ 日期时间类的使用
  • ✅ 包装类的装箱拆箱
  • ✅ System类的常用方法

重点记忆

  1. Object是所有类的父类
  2. Integer缓存范围是-128~127
  3. 日期格式化用SimpleDateFormat或LocalDateTime
  4. 自动拆箱注意NPE问题

下一步预告
Day11我们将学习异常处理——try-catch、自定义异常、异常体系等。


参考资料

  1. Oracle官方文档 - Object类
  2. Baeldung - Java 8 Date Time Tutorial

互动话题:你在使用日期类的时候,有没有被Date和LocalDateTime搞混过?欢迎在评论区分享!

如果这篇文章对你有帮助,欢迎点赞、收藏!这是【JavaSE全面教学】系列的第10篇,关注我看完整套教程 👇


本文为【JavaSE全面教学】系列第10篇,持续更新中…

更多推荐