Java(四)

35 阅读5分钟

本章覆盖日常开发最高频的类型:String、基本类型包装类、Java 8 日期时间 API。


1. String 不可变(Immutability)

概念

String 对象创建后内容不可变;任何「修改」都会产生新 String 对象。

原理

  • 内部 byte[](JDK 9+ 紧凑表示)与 hash 等被 final 保护。
  • 字符串常量池(String Pool):字面量 "abc" 可能复用同一实例。
  • 好处:线程安全、可安全共享、适合作为 Map 键;代价:频繁拼接产生垃圾。

完整示例

package com.example.demo;

public class StringImmutableDemo {
    public static void main(String[] args) {
        String a = "hello";
        String b = a;
        b = b + " world";   // 新对象,a 仍是 "hello"
        System.out.println(a);
        System.out.println(b);

        // 大量拼接用 StringBuilder
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 5; i++) {
            sb.append(i).append(',');
        }
        String joined = sb.toString();
        System.out.println(joined);
    }
}

练习

循环 1 万次 s += "x" vs StringBuilder

完整例子

package com.example.demo;

public class StringPerfDemo {
    public static void main(String[] args) {
        long t1 = System.currentTimeMillis();
        String s = "";
        for (int i = 0; i < 10_000; i++) s += "x";
        System.out.println("+= ms=" + (System.currentTimeMillis() - t1));

        long t2 = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10_000; i++) sb.append('x');
        String built = sb.toString();
        System.out.println("SB ms=" + (System.currentTimeMillis() - t2) + " len=" + built.length());
    }
}

常见坑

  • 在循环里 result += line 性能差。
  • 以为修改了 String 字符 — 没有 API 能原地改(charAt 只读)。

2. == 与 equals

概念

  • ==:基本类型比值;引用类型比引用地址是否同一对象。
  • equals(Object):逻辑相等,类需正确重写;String 已重写为比字符内容。

原理

String s1 = "abc";
String s2 = new String("abc");
String s3 = "abc";
// s1 == s3 可能 true(常量池)
// s1 == s2 false(堆上新对象)
// s1.equals(s2) true

重写 equals 通常也要重写 hashCode(04 章详述)。

完整示例

package com.example.demo;

import java.util.Objects;

public class EqualsDemo {
    static class User {
        private final Long id;
        private String name;

        User(Long id, String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof User other)) return false;
            return Objects.equals(id, other.id);
        }

        @Override
        public int hashCode() {
            return Objects.hash(id);
        }
    }

    public static void main(String[] args) {
        System.out.println("abc".equals("abc"));
        System.out.println(new User(1L, "a").equals(new User(1L, "b")));
    }
}

练习

比较 "Java" 与 new String("Java") 的 == 与 equals 结果并解释。

完整例子

package com.example.demo;

public class EqualsExperiment {
    public static void main(String[] args) {
        String literal = "Java";
        String created = new String("Java");

        // false:字面量在常量池,new 在堆上是另一对象
        System.out.println(literal == created);

        // true:equals 比内容
        System.out.println(literal.equals(created));

        // 常为 true(池化),但业务代码勿用 == 比字符串内容
        System.out.println("Java" == "Java");
    }
}

常见坑

  • str.equals(maybeNull) NPE — 写成 "常量".equals(str) 或 Objects.equals(a, b)。
  • 只重写 equals 不重写 hashCode,HashMap/HashSet 行为异常。

3. 常用 String API

概念

length、isEmpty/isBlank(11+)、substring、indexOf、split、trim/strip、formatted/format。

完整示例

package com.example.demo;

import java.util.Arrays;

public class StringApiDemo {
    public static void main(String[] args) {
        String csv = " alice , bob , carol ";
        String[] parts = Arrays.stream(csv.split(","))
                .map(String::strip)
                .filter(s -> !s.isEmpty())
                .toArray(String[]::new);
        System.out.println(Arrays.toString(parts));

        String msg = "order %d amount %.2f".formatted(1001, 88.5);
        System.out.println(msg);
    }
}

练习

给定 "a,b,c,,d",split 后去空串,输出 a|b|c|d(用 StringBuilder)。

完整例子

package com.example.demo;

public class CsvJoinDemo {
    public static void main(String[] args) {
        String raw = "a,b,c,,d";
        StringBuilder sb = new StringBuilder();
        for (String part : raw.split(",")) {
            if (part.isEmpty()) continue;
            if (sb.length() > 0) sb.append('|');
            sb.append(part);
        }
        System.out.println(sb); // a|b|c|d
    }
}

常见坑

  • split(",") 参数是正则,. 要转义 \\.。
  • substring 下标越界抛异常。

4. 包装类与自动装箱(Autoboxing)

概念

每个基本类型有包装类:Integer、Long、Double、Boolean…
装箱:基本类型 → 包装类;拆箱:包装 → 基本类型。编译器自动插入 valueOf / xxxValue()。

原理

  • Integer.valueOf(127) 与 Integer.valueOf(128) 可能池化/cache 行为不同(-128~127 默认缓存)。
  • 包装类可为 null,拆箱时 NPE 或 NPE on unboxing。

完整示例

package com.example.demo;

import java.util.Optional;

public class WrapperDemo {
    public static void main(String[] args) {
        Integer a = 100;      // autobox
        int b = a;              // unbox
        Integer c = null;
        // int d = c;           // NPE

        Optional<Integer> opt = Optional.ofNullable(a);
        int safe = opt.orElse(0);

        // 比较用 equals,勿用 ==
        Integer x = 200, y = 200;
        System.out.println(x.equals(y));
    }
}

练习

方法 sum(Integer a, Integer b) 处理 null 当 0。

完整例子

package com.example.demo;

public class NullSafeSum {
    static int sum(Integer a, Integer b) {
        int x = a == null ? 0 : a;
        int y = b == null ? 0 : b;
        return x + y;
    }

    public static void main(String[] args) {
        System.out.println(sum(3, null)); // 3
        System.out.println(sum(null, null)); // 0
    }
}

常见坑

  • Integer i = null; if (i == 1) 自动拆箱 NPE。
  • 用 == 比较 Integer 大数值 — 用 equals 或基本类型 int。

5. LocalDateTime 与 java.time

概念

Java 8+ java.time 替代旧的 Date/Calendar:LocalDate、LocalTime、LocalDateTime、ZonedDateTime、Instant。

原理

不可变、线程安全、清晰 API。持久化/JSON 时常用 ISO-8601 字符串或 epoch 毫秒(与 DB 交互见后续章节)。

完整示例

package com.example.demo;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;

public class DateTimeDemo {
    public static void main(String[] args) {
        LocalDate today = LocalDate.now();
        LocalDateTime created = LocalDateTime.of(2026, 7, 18, 21, 30);

        LocalDateTime deadline = created.plusDays(7);
        long days = ChronoUnit.DAYS.between(today, deadline);

        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        System.out.println(created.format(fmt));
        System.out.println("days until deadline (signed): " + days);
    }
}

练习

输入生日 LocalDate,计算距今天数(Period 或 ChronoUnit)。

完整例子

package com.example.demo;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

public class BirthdayDays {
    public static void main(String[] args) {
        LocalDate birthday = LocalDate.of(2000, 1, 1);
        LocalDate today = LocalDate.now();
        long days = ChronoUnit.DAYS.between(birthday, today);
        System.out.println("距今天数: " + days);
    }
}

常见坑

  • LocalDateTime 没有时区;跨时区业务用 ZonedDateTime 或存 UTC Instant。
  • 旧 API SimpleDateFormat 非线程安全,多线程共享会错;新项目用 DateTimeFormatter。

6. Optional(简要)

概念

Optional<T> 显式表示「可能有值」,减少 NPE;勿滥用为字段类型。

完整示例

package com.example.demo;

import java.util.Optional;

public class OptionalDemo {
    static Optional<String> findUsername(Long id) {
        if (id == null || id <= 0) return Optional.empty();
        return Optional.of("user-" + id);
    }

    public static void main(String[] args) {
        findUsername(1L).ifPresent(u -> System.out.println(u));
        String name = findUsername(null).orElse("guest");
        System.out.println(name);
    }
}

常见坑

  • Optional.get() 无值时抛异常 — 用 orElse / orElseThrow。
  • 把 Optional 作为 JSON 字段返回给前端 — 通常直接 null 或省略键。

本章练习(概要)

  1. 实现 maskEmail("alice@example.com") → a***@example.com(规则自定,文档说明)。
  2. 解析 "2026-07-18 14:30:00" 为 LocalDateTime,加 2 小时输出。
  3. 用 Map<String, Integer> 统计句子中单词出现次数(预习 04 章)。

提示见 exercises.md。

完整例子

package com.example.demo.exercise;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;

public class Chapter03Exercises {
    /** 规则:本地部分保留首字符,其余用 ***,域名不变 */
    static String maskEmail(String email) {
        int at = email.indexOf('@');
        if (at <= 0) return email;
        String local = email.substring(0, at);
        String domain = email.substring(at);
        String masked = local.charAt(0) + "***";
        return masked + domain;
    }

    public static void main(String[] args) {
        System.out.println(maskEmail("alice@example.com"));

        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        LocalDateTime dt = LocalDateTime.parse("2026-07-18 14:30:00", fmt).plusHours(2);
        System.out.println(dt.format(fmt));

        String sentence = "java is fun and java is popular";
        Map<String, Integer> freq = new HashMap<>();
        for (String w : sentence.toLowerCase().split("\\s+")) {
            freq.merge(w, 1, Integer::sum);
        }
        System.out.println(freq);
    }
}

本章小结

主题要点
String不可变;拼接用 StringBuilder
equals内容相等;常配合 hashCode
包装类装箱/拆箱;null 拆箱 NPE
java.timeLocalDateTime + DateTimeFormatter