重新认识Java

基础

程序的入口

简单来说,程序的入口并没有变,依然是 main 方法,且一个项目应当只有一个类中有 main 方法。

// src/main/java/org/group/program/Main.java
package org.group.program;

public class Main {
    static void main(String[] args) {
        System.out.println("name: ");
        Scanner scanner = new Scanner(System.in);
        String name = scanner.nextLine();
        System.out.println("Hello " + name + " !");
        scanner.close();
    }
}

但是,随着现代高级语言的发展,试运行一段简单的代码片段没有必要非要去声明一个包 package 和一个类 Class ,这些过于臃肿了。

随着 java 25 这个 LTS 版本的到来,java 语言多了一种“新”的文件类型,紧凑的源文件( Compact Source File )。

// src/main/java/Main.java
void main() {
    String name = IO.readln();
    IO.println("Hello " + name + " !");
}

这种文件其实并不“新”,还是 .java 这种后缀文件。它的特殊性有两点:

  1. 必须放在 src/main/java 目录下,不能放到任何 package 包中。
  2. void main() 方法签名固定,没有入参,没有 public static 。

void main() 实际上在编译后会被放在一个同文件名的 final 类中,该类会继承 java.lang.Object ,并且该类不实现任何接口。

如果是新手在第一次遇到 System.out.println("Hello Java!") 的时候,都会很迷茫。因为这实际上是默认了开发者应当知道 java.lang.System 类下有 public static final PrintStream out 这个静态变量,同时还要知道 java.io.PrintStream 这个类有 public void print(Object obj) 这个公有方法。而看到 Scanner scanner = new Scanner(System.in) 还有 scanner.nextLine() 与 scanner.close() 时,哦天呐,这也太复杂了!

好在 java 25 迎来了对此的“简化”,提供了 java.lang.IO 类。

public final class IO {
    private IO() { throw new Error("no instances"); }
    
    public static void println(Object obj) { System.out.println(obj); }
    public static void println() { System.out.println(); }
    public static void print(Object obj) {
        var out = System.out;
        out.print(obj);
        out.flush();
    }
    
    public static String readln() {
        try {
            return reader().readLine();
        } catch (IOException ioe) {
            throw new IOError(ioe);
        }
    }
    public static String readln(String prompt) {
        print(prompt);
        return readln();
    }
    private static BufferedReader br;
    static synchronized BufferedReader reader() {
        if (br == null) {
            String enc = System.getProperty("stdin.encoding", "");
            Charset cs = Charset.forName(enc, StandardCharsets.UTF_8);
            br = new BufferedReader(new InputStreamReader(System.in, cs));
        }
        return br;
    }
}

可以发现,新的 IO 类仅仅是对已有的方法进行了简单封装,并且封装后的 readln() 与 println() 方法更为对应。

变量的声明

作为一个静态编译型的早期高级语言,变量声明的语法着重在于让编译器能够快速稳定的确定编译的具体内容。

String message = "Hello world!";
Path path = Path.of("debug.log");
InputStream stream = Files.newInputStream(path);

随着现代编译器的发展,以及新一代高级编程语言的类型推断特性的冲击下,变量的声明应当更为简洁,将编程者的注意力从语法中转移到实际业务上。如果变量类型名称在业务中很长,这会让一行变量声明代码变得很啰嗦。

好在,从 java 10 开始,有了 var 关键字。

var message = "Hello world!";
var path = Path.of("debug.log");
var stream = Files.newInputStream(path);

var 关键字能在各种有支持 java 语法高亮的编辑器中,明确让代码阅读者意识到,这里有一个新的变量。

但是要注意 var 变量的使用有一些限制:

  1. 只能在方法的代码块内使用。
  2. 不能在类属性、方法入参处使用。如果类型不确定,会导致方法签名相关错误。
  3. var 声明的变量必须立马明确赋值,绝对不能赋值为 null ,因为 null 没有类型,会导致编译报错。

实际上,在编译成 .class 文件后,var 关键字会被替换成原本数据类型。

switch 条件分支

在 java 14 之前,我们使用 switch 对诸如枚举这样的变量进行条件分支拆分,但说不上美观,并且通常会占据很多的行数。

int len = 0;
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        len = 6;
        break;
    case TUESDAY:
        len = 7;
        break;
    case THURSDAY:
    case SATURDAY:
        len = 8;
        break;
    case WEDNESDAY:
        len = 9;
        break;
    default:
        len = 0;
        break;
}

在 java 14 之后,多了新的 switch 表达式语法,他能让 switch 变得更加简短美观。

int len = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    case THURSDAY, SATURDAY     -> 8;
    case WEDNESDAY              -> 9;
    default                     -> 0;
};

新的语法,通过用 , 连接同运行逻辑的条件,case 不用写 : 的同时,对应执行逻辑默认包含 break,从而减少了在不写 break 让 case 下坠合并的“魔法”行为时可能产生错误逻辑的 bug 可能性,还提供了直接返回结果的能力。不过要注意,如果进行赋值运算,不要忘了在 switch 的 } 后的 ; 。

public int calculate(Day d) {
    return switch (d) {
        case SATURDAY, SUNDAY -> 0;
        default -> {
            int remainingWorkDays = 5 - d.ordinal();
            yield remainingWorkDays;
        }
    };
}

是的,java 14 后可以像上方代码展示的代码一样,方法只需要一行了,直接 return 了 switch 的结果,并且随着 switch 表达式的到来,为了与原本方法的 return 做区分,使用新的 yield 关键字作为 switch 自己的返回,而 yield 在简单最后一行逻辑即返回值的情况下还可以被省略。

此时可能会回想起另一个当初不能使用 switch 必须使用 if 的场景,对象类型区分与 instanceof 关键字。

if (data.get("key") instanceof String s) {
  log.info(s);
} else if (data.get("key") instanceof Double s) {
  log.info(s);
} else if (data.get("key") instanceof Integer s) {
  log.info(s);
}

从 java 21 开始, case [具体类型] [变量别称] -> {} ,终于 switch 能够判断具体数据的类型了。

switch (data.get("key1")) {
  case String s  -> log.info(s);
  case Double d  -> log.info(d.toString());
  case Integer i -> log.info(i.toString());
  default        -> log.info("");
}

注意,小心 switch(obj) 中的 obj 为 null !

lambda

中级

高阶

拓展