从Java8到Java23值得期待的x个特性

你好,我是看山。

本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。

概述

从 2017 年开始,Java 版本更新遵循每六个月发布一次的节奏,LTS版本则每两年发布一次,以快速验证新特性,推动 Java 的发展。

Java版本分布

数据来源2024 State of the Java Ecosystem

一些小特性

增强 String

String中新增的方法:repeat、strip、stripLeading、stripTrailing、isBlank、lines、indent 和 transform。

这些方法还是挺有用的,以前我们可能需要借助第三方类库(比如 Apache 出品的 commons-lang)中的工具类,现在可以直接使用嫡亲方法了。

repeat

repeat是实例方法,顾名思义,这个方法是返回给定字符串的重复值的,参数是int类型,传参的时候需要注意:

  • 如果重复次数小于 0 会抛出IllegalArgumentException异常;
  • 如果重复次数为 0 或者字符串本身是空字符串,将返回空字符串;
  • 如果重复次数为 1 直接返回本身;
  • 如果字符串重复指定次数后,长度超过Integer.MAX_VALUE,会抛出OutOfMemoryError`错误。

用法很简单:

@Test
void testRepeat() {
    String output = "foo ".repeat(2) + "bar";
    assertEquals("foo foo bar", output);
}

小而美的一个工具方法。

strip、stripLeading、stripTrailing

strip方法算是trim方法的增强版,trim方法可以删除字符串两侧的空白字符(空格、tab 键、换行符),但是对于Unicode的空白字符无能为力,strip补足这一短板。

用起来是这样的:

@Test
void testTrip() {
    final String output = "
      hello   \u2005".strip();
    assertEquals("hello", output);

    final String trimOutput = "
      hello   \u2005".trim();
    assertEquals("hello   \u2005", trimOutput);
}

对比一下可以看到,trim方法的清理功能稍弱。

stripLeadingstripTrailingstrip类似,区别是一个清理头,一个清理尾。用法如下:

@Test
void testTripLeading() {
    final String output = "
      hello   \u2005".stripLeading();
    assertEquals("hello   \u2005", output);
}

@Test
void testTripTrailing() {
    final String output = "
      hello   \u2005".stripTrailing();
    assertEquals("
      hello", output);
}

isBlank

这个方法是用于判断字符串是否都是空白字符,除了空格、tab 键、换行符,也包括Unicode的空白字符。

用法很简单:

@Test
void testIsBlank() {
    assertTrue("
    \u2005".isBlank());
}

lines

最后这个方法是将字符串转化为字符串Stream类型,字符串分隔依据是换行符:\n\r\r\n,用法如下:

@Test
void testLines() {
    final String multiline = "This is

a multiline
string.";
    final String output = multiline.lines()
            .filter(Predicate.not(String::isBlank))
            .collect(Collectors.joining(" "));
    assertEquals("This is a multiline string.", output);
}

indent

indent方法是对字符串每行(使用\r\n分隔)数据缩进指定空白字符,参数是 int 类型。

如果参数大于 0,就缩进指定数量的空格;如果参数小于 0,就将左侧的空字符删除指定数量,即右移。

我们看下源码:

public String indent(int n) {
    if (isEmpty()) {
        return "";
    }
    Stream<String> stream = lines();
    if (n > 0) {
        final String spaces = " ".repeat(n);
        stream = stream.map(s -> spaces + s);
    } else if (n == Integer.MIN_VALUE) {
        stream = stream.map(s -> s.stripLeading());
    } else if (n < 0) {
        stream = stream.map(s -> s.substring(Math.min(-n, s.indexOfNonWhitespace())));
    }
    return stream.collect(Collectors.joining("
", "", "
"));
}

indent最后会将多行数据通过Collectors.joining("\n", "", "\n")方法拼接,结果会有两点需要注意:

  • \r会被替换成\n
  • 如果原字符串是多行数据,最后一行的结尾没有\n,最后会补上一个\n,即多了一个空行。

我们看下测试代码:

@Test
void testIndent() {
    final String text = "             你好,我是看山。
 \u0020\u2005Java12 的 新特性。
 欢迎三连+关注哟";
    assertEquals("                 你好,我是看山。
     \u0020\u2005Java12 的 新特性。
    欢迎三连+关注哟、n", text.indent(4));
    assertEquals("     你好,我是看山。
\u2005Java12 的 新特性。
 欢迎三连+关注哟、n", text.indent(-2));

    final String text2 = "山水有相逢";
    assertEquals("山水有相逢", text2);
}

transform

我们再来看看transform方法,源码一目了然:

public <R> R transform(Function<? super String, ? extends R> f) {
    return f.apply(this);
}

通过传入的Function对当前字符串进行转换,转换结果由Function决定。比如,我们要对字符串反转:

@Test
void testTransform() {
    final String text = "看山是山";
    final String reverseText = text.transform(s -> new StringBuilder(s).reverse().toString());
    assertEquals("山是山看", reverseText);
}

增强文件读写(Java 11)

本次更新在Files中增加了两个方法:readStringwriteStringwriteString作用是将指定字符串写入文件,readString作用是从文件中读出内容到字符串。是一个对Files工具类的增强,封装了对输出流、字节等内容的操作。

用法比较简单:

@Test
void testReadWriteString() throws IOException {
    final Path tmpPath = Path.of("./");
    final Path tempFile = Files.createTempFile(tmpPath, "demo", ".txt");
    final Path filePath = Files.writeString(tempFile, "看山 howardliu.cn
 公众号:看山的小屋");
    assertEquals(tempFile, filePath);

    final String fileContent = Files.readString(filePath);
    assertEquals("看山 howardliu.cn
 公众号:看山的小屋", fileContent);

    Files.deleteIfExists(filePath);
}

readStringwriteString还可以指定字符集,不指定默认使用StandardCharsets.UTF\_8字符集,可以应对大部分场景了。

增强函数 Predicate(Java 11)

这个也是方法增强,在以前,我们在Stream中的filter方法判断否的时候,一般需要!运算,比如我们想要找到字符串列表中的数字,可以这样写:

final List<String> list = Arrays.asList("1", "a");
final List<String> nums = list.stream()
        .filter(NumberUtils::isDigits)
        .collect(Collectors.toList());
Assertions.assertEquals(1, nums.size());
Assertions.assertTrue(nums.contains("1"));

想要找到非数字的,filter方法写的就会用到!非操作:

final List<String> notNums = list.stream()
        .filter(x -> !NumberUtils.isDigits(x))
        .collect(Collectors.toList());
Assertions.assertEquals(1, notNums.size());
Assertions.assertTrue(notNums.contains("a"));

Predicate增加not方法,可以更加简单的实现非操作:

final List<String> notNums2 = list.stream()
        .filter(Predicate.not(NumberUtils::isDigits))
        .collect(Collectors.toList());
Assertions.assertEquals(1, notNums2.size());
Assertions.assertTrue(notNums2.contains("a"));

有些教程还会推崇静态引入,比如在头部使用import static java.util.function.Predicate.not,这样在函数式编程时,可以写更少的代码,语义更强,比如:

final List<String> notNums2 = list.stream()
        .filter(not(NumberUtils::isDigits))
        .collect(toList());

未命名模式和变量(Java 22)

该特性使用下划线字符 \_ 来表示未命名的模式和变量,从而简化代码并提高代码可读性和可维护性。

比如:

public static void main(String[] args) {
    var _ = new Point(1, 2);
}

record Point(int x, int y) {
}

这个可以用在任何定义变量的地方,比如:

  • ... instanceof Point(\_, int y)
  • r instanceof Point \_
  • switch …… case Box(\_)
  • for (Order \_ : orders)
  • for (int i = 0, \_ = sideEffect(); i < 10; i++)
  • try { ... } catch (Exception \_) { ... } catch (Throwable \_) { ... }

只要是这个不准备用,可以一律使用\_代替。

Markdown格式文档注释(Java 23)

Markdown是一种轻量级的标记语言,可用于在纯文本文档中添加格式化元素,具体语法可以参考Markdown Guide。本文就是使用Markdown语法编写的。

在Java注释中引入Markdown,目标是使API文档注释以源代码形式更易于编写和阅读。主要收益包括:

  • 提高文档编写的效率:Markdown语法相比HTML更为简洁,开发者可以更快地编写和修改文档注释。
  • 增强文档的可读性:Markdown格式的文档在源代码中更易于阅读,有助于开发者快速理解API的用途和行为。
  • 促进文档的一致性:通过支持Markdown,可以确保文档风格的一致性,减少因格式问题导致的文档混乱。
  • 简化文档维护:Markdown格式的文档注释更易于维护和更新,特别是在多人协作的项目中,可以减少因文档格式问题导致的沟通成本。

具体使用方式是在注释前面增加///,比如java.lang.Object.hashCode的注释:

/**
 * Returns a hash code value for the object. This method is
 * supported for the benefit of hash tables such as those provided by
 * {@link java.util.HashMap}.
 * <p>
 * The general contract of {@code hashCode} is:
 * <ul>
 * <li>Whenever it is invoked on the same object more than once during
 *     an execution of a Java application, the {@code hashCode} method
 *     must consistently return the same integer, provided no information
 *     used in {@code equals} comparisons on the object is modified.
 *     This integer need not remain consistent from one execution of an
 *     application to another execution of the same application.
 * <li>If two objects are equal according to the {@link
 *     #equals(Object) equals} method, then calling the {@code
 *     hashCode} method on each of the two objects must produce the
 *     same integer result.
 * <li>It is <em>not</em> required that if two objects are unequal
 *     according to the {@link #equals(Object) equals} method, then
 *     calling the {@code hashCode} method on each of the two objects
 *     must produce distinct integer results.  However, the programmer
 *     should be aware that producing distinct integer results for
 *     unequal objects may improve the performance of hash tables.
 * </ul>
 *
 * @implSpec
 * As far as is reasonably practical, the {@code hashCode} method defined
 * by class {@code Object} returns distinct integers for distinct objects.
 *
 * @return  a hash code value for this object.
 * @see     java.lang.Object#equals(java.lang.Object)
 * @see     java.lang.System#identityHashCode
 */

如果使用JEP 467的Markdown方式:

/// Returns a hash code value for the object. This method is
/// supported for the benefit of hash tables such as those provided by
/// [java.util.HashMap].
///
/// The general contract of `hashCode` is:
///
///   - Whenever it is invoked on the same object more than once during
///     an execution of a Java application, the `hashCode` method
///     must consistently return the same integer, provided no information
///     used in `equals` comparisons on the object is modified.
///     This integer need not remain consistent from one execution of an
///     application to another execution of the same application.
///   - If two objects are equal according to the
///     [equals][#equals(Object)] method, then calling the
///     `hashCode` method on each of the two objects must produce the
///     same integer result.
///   - It is _not_ required that if two objects are unequal
///     according to the [equals][#equals(Object)] method, then
///     calling the `hashCode` method on each of the two objects
///     must produce distinct integer results.  However, the programmer
///     should be aware that producing distinct integer results for
///     unequal objects may improve the performance of hash tables.
///
/// @implSpec
/// As far as is reasonably practical, the `hashCode` method defined
/// by class `Object` returns distinct integers for distinct objects.
///
/// @return  a hash code value for this object.
/// @see     java.lang.Object#equals(java.lang.Object)
/// @see     java.lang.System#identityHashCode

简单两种写法的差异,相同注释,Markdown的写法更加简洁:

Object hashcode注释差异

文末总结

想要了解各版本的详细特性,可以从从小工到专家的 Java 进阶之旅 系列专栏中查看。

青山不改,绿水长流,我们下次见。

推荐阅读


你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。

👇🏻欢迎关注我的公众号「看山的小屋」,领取精选资料👇🏻

公众号:看山的小屋