拥抱变化,面向Java17,Java8-18全系列特性详解_jdk18和java8

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新大数据全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注大数据)
img

正文

上述代码的输出结果如下:

Full Name is set? false
Full Name: [none]
Hey Stranger!

再看下另一个简单的例子:

Optional<String> firstName = Optional.of( "Tom" );
System.out.println( "First Name is set? " + firstName.isPresent() );        
System.out.println( "First Name: " + firstName.orElseGet( () -> "[none]" ) ); 
System.out.println( firstName.map(s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
System.out.println();

这个例子的输出是:

First Name is set? true
First Name: Tom
Hey Tom!

如果想了解更多的细节,请参考官方文档
Optional源码

package java.util;

import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

/\*\*
 \* A container object which may or may not contain a non-null value.
 \* If a value is present, {@code isPresent()} will return {@code true} and
 \* {@code get()} will return the value.
 \*
 \* <p>Additional methods that depend on the presence or absence of a contained
 \* value are provided, such as {@link #orElse(java.lang.Object) orElse()}
 \* (return a default value if value not present) and
 \* {@link #ifPresent(java.util.function.Consumer) ifPresent()} (execute a block
 \* of code if the value is present).
 \*
 \* <p>This is a <a href="../lang/doc-files/ValueBased.html">value-based</a>
 \* class; use of identity-sensitive operations (including reference equality
 \* ({@code ==}), identity hash code, or synchronization) on instances of
 \* {@code Optional} may have unpredictable results and should be avoided.
 \*
 \* @since 1.8
 \*/
public final class Optional<T> {
    /\*\*
 \* Common instance for {@code empty()}.
 \*/
    private static final Optional<?> EMPTY = new Optional<>();

    /\*\*
 \* If non-null, the value; if null, indicates no value is present
 \*/
    private final T value;

    /\*\*
 \* Constructs an empty instance.
 \*
 \* @implNote Generally only one empty instance, {@link Optional#EMPTY},
 \* should exist per VM.
 \* 
 \* 无参构造
 \*/
    private Optional() {
        this.value = null;
    }

    /\*\*
 \* Returns an empty {@code Optional} instance. No value is present for this
 \* Optional.
 \*
 \* @apiNote Though it may be tempting to do so, avoid testing if an object
 \* is empty by comparing with {@code ==} against instances returned by
 \* {@code Option.empty()}. There is no guarantee that it is a singleton.
 \* Instead, use {@link #isPresent()}.
 \*
 \* @param <T> Type of the non-existent value
 \* @return an empty {@code Optional}
 \* 
 \* 返回一个空Optional实例。
 \*/
    public static<T> Optional<T> empty() {
        @SuppressWarnings("unchecked")
        Optional<T> t = (Optional<T>) EMPTY;
        return t;
    }

    /\*\*
 \* Constructs an instance with the value present.
 \*
 \* @param value the non-null value to be present
 \* @throws NullPointerException if value is null
 \*
 \* 有参构造
 \*/
    private Optional(T value) {
        this.value = Objects.requireNonNull(value);
    }

    /\*\*
 \* Returns an {@code Optional} with the specified present non-null value.
 \*
 \* @param <T> the class of the value
 \* @param value the value to be present, which must be non-null
 \* @return an {@code Optional} with the value present
 \* @throws NullPointerException if value is null
 \*
 \* 返回一个具有指定当前非空值的Optional实例。
 \*/
    public static <T> Optional<T> of(T value) {
        return new Optional<>(value);
    }

    /\*\*
 \* Returns an {@code Optional} describing the specified value, if non-null,
 \* otherwise returns an empty {@code Optional}.
 \*
 \* @param <T> the class of the value
 \* @param value the possibly-null value to describe
 \* @return an {@code Optional} with a present value if the specified value
 \* is non-null, otherwise an empty {@code Optional}
 \*
 \* 如果非空,返回一个指定值Optional实例,否则返回一个空Optional。
 \*/
    public static <T> Optional<T> ofNullable(T value) {
        return value == null ? empty() : of(value);
    }

    /\*\*
 \* If a value is present in this {@code Optional}, returns the value,
 \* otherwise throws {@code NoSuchElementException}.
 \*
 \* @return the non-null value held by this {@code Optional}
 \* @throws NoSuchElementException if there is no value present
 \*
 \* @see Optional#isPresent()
 \*
 \* 如果 this 中存在Optional值,则返回该值,否则抛出NoSuchElementException。
 \*/
    public T get() {
        if (value == null) {
            throw new NoSuchElementException("No value present");
        }
        return value;
    }

    /\*\*
 \* Return {@code true} if there is a value present, otherwise {@code false}.
 \*
 \* @return {@code true} if there is a value present, otherwise {@code false}
 \*
 \* 如果存在值(不为null)则返回true,否则返回false。
 \*/
    public boolean isPresent() {
        return value != null;
    }

    /\*\*
 \* If a value is present, invoke the specified consumer with the value,
 \* otherwise do nothing.
 \*
 \* @param consumer block to be executed if a value is present
 \* @throws NullPointerException if value is present and {@code consumer} is
 \* null
 \*
 \* 如果存在值,则使用该值调用指定的使用者,否则不执行任何操作。
 \*/
    public void ifPresent(Consumer<? super T> consumer) {
        if (value != null)
            consumer.accept(value);
    }

    /\*\*
 \* If a value is present, and the value matches the given predicate,
 \* return an {@code Optional} describing the value, otherwise return an
 \* empty {@code Optional}.
 \*
 \* @param predicate a predicate to apply to the value, if present
 \* @return an {@code Optional} describing the value of this {@code Optional}
 \* if a value is present and the value matches the given predicate,
 \* otherwise an empty {@code Optional}
 \* @throws NullPointerException if the predicate is null
 \*
 \* 如果存在一个值,并且该值与给定的谓词匹配,则返回一个Optional描述该值的值,否则返回一个空值Optional。
 \*/
    public Optional<T> filter(Predicate<? super T> predicate) {
        Objects.requireNonNull(predicate);
        if (!isPresent())
            return this;
        else
            return predicate.test(value) ? this : empty();
    }

    /\*\*
 \* If a value is present, apply the provided mapping function to it,
 \* and if the result is non-null, return an {@code Optional} describing the
 \* result. Otherwise return an empty {@code Optional}.
 \*
 \* @apiNote This method supports post-processing on optional values, without
 \* the need to explicitly check for a return status. For example, the
 \* following code traverses a stream of file names, selects one that has
 \* not yet been processed, and then opens that file, returning an
 \* {@code Optional<FileInputStream>}:
 \*
 \* <pre>{@code
 \* Optional<FileInputStream> fis =
 \* names.stream().filter(name -> !isProcessedYet(name))
 \* .findFirst()
 \* .map(name -> new FileInputStream(name));
 \* }</pre>
 \*
 \* Here, {@code findFirst} returns an {@code Optional<String>}, and then
 \* {@code map} returns an {@code Optional<FileInputStream>} for the desired
 \* file if one exists.
 \*
 \* @param <U> The type of the result of the mapping function
 \* @param mapper a mapping function to apply to the value, if present
 \* @return an {@code Optional} describing the result of applying a mapping
 \* function to the value of this {@code Optional}, if a value is present,
 \* otherwise an empty {@code Optional}
 \* @throws NullPointerException if the mapping function is null
 \*
 \* 如果存在值,则对其应用提供的映射函数,如果结果为非 null,则返回Optional描述结果。
 \*/
    public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
        Objects.requireNonNull(mapper);
        if (!isPresent())
            return empty();
        else {
            return Optional.ofNullable(mapper.apply(value));
        }
    }

    /\*\*
 \* If a value is present, apply the provided {@code Optional}-bearing
 \* mapping function to it, return that result, otherwise return an empty
 \* {@code Optional}. This method is similar to {@link #map(Function)},
 \* but the provided mapper is one whose result is already an {@code Optional},
 \* and if invoked, {@code flatMap} does not wrap it with an additional
 \* {@code Optional}.
 \*
 \* @param <U> The type parameter to the {@code Optional} returned by
 \* @param mapper a mapping function to apply to the value, if present
 \* the mapping function
 \* @return the result of applying an {@code Optional}-bearing mapping
 \* function to the value of this {@code Optional}, if a value is present,
 \* otherwise an empty {@code Optional}
 \* @throws NullPointerException if the mapping function is null or returns
 \* a null result
 \*
 \* 如果存在值,则将提供的Optional-bearing 映射函数应用于它,返回该结果,否则返回空 Optional。
 \*/
    public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
        Objects.requireNonNull(mapper);
        if (!isPresent())
            return empty();
        else {
            return Objects.requireNonNull(mapper.apply(value));
        }
    }

    /\*\*
 \* Return the value if present, otherwise return {@code other}.
 \*
 \* @param other the value to be returned if there is no value present, may
 \* be null
 \* @return the value, if present, otherwise {@code other}
 \*
 \* 对自身判断,如果存在则返回自身,否则返回other。
 \* 对自身判断,如果不为null则返回自身,否则返回一个指定的值。
 \*/
    public T orElse(T other) {
        return value != null ? value : other;
    }

    /\*\*
 \* Return the value if present, otherwise invoke {@code other} and return
 \* the result of that invocation.
 \*
 \* @param other a {@code Supplier} whose result is returned if no value
 \* is present
 \* @return the value if present otherwise the result of {@code other.get()}
 \* @throws NullPointerException if value is not present and {@code other} is
 \* null
 \*
 \* 如果存在则返回该值,否则调用other并返回该调用的结果。
 \*/
    public T orElseGet(Supplier<? extends T> other) {
        return value != null ? value : other.get();
    }

    /\*\*
 \* Return the contained value, if present, otherwise throw an exception
 \* to be created by the provided supplier.
 \*
 \* @apiNote A method reference to the exception constructor with an empty
 \* argument list can be used as the supplier. For example,
 \* {@code IllegalStateException::new}
 \*
 \* @param <X> Type of the exception to be thrown
 \* @param exceptionSupplier The supplier which will return the exception to
 \* be thrown
 \* @return the present value
 \* @throws X if there is no value present
 \* @throws NullPointerException if no value is present and
 \* {@code exceptionSupplier} is null
 \*
 \* 如果存在(不为null),则返回该值,否则抛出由提供的供应商创建的异常。
 \*/
    public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
        if (value != null) {
            return value;
        } else {
            throw exceptionSupplier.get();
        }
    }

    /\*\*
 \* Indicates whether some other object is "equal to" this Optional. The
 \* other object is considered equal if:
 \* <ul>
 \* <li>it is also an {@code Optional} and;
 \* <li>both instances have no value present or;
 \* <li>the present values are "equal to" each other via {@code equals()}.
 \* </ul>
 \*
 \* @param obj an object to be tested for equality
 \* @return {code true} if the other object is "equal to" this object
 \* otherwise {@code false}
 \*
 \* 判断某个其他对象是否“等于”此 Optional。 相等返回true,不想等返回false
 \*/
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (!(obj instanceof Optional)) {
            return false;
        }

        Optional<?> other = (Optional<?>) obj;
        return Objects.equals(value, other.value);
    }

    /\*\*
 \* Returns the hash code value of the present value, if any, or 0 (zero) if
 \* no value is present.
 \*
 \* @return hash code value of the present value or 0 if no value is present
 \*
 \* 返回当前值的哈希码值(如果有);如果不存在值,则返回 0(零)。
 \*/
    @Override
    public int hashCode() {
        return Objects.hashCode(value);
    }

    /\*\*
 \* Returns a non-empty string representation of this Optional suitable for
 \* debugging. The exact presentation format is unspecified and may vary
 \* between implementations and versions.
 \*
 \* @implSpec If a value is present the result must include its string
 \* representation in the result. Empty and present Optionals must be
 \* unambiguously differentiable.
 \*
 \* @return the string representation of this instance
 \*
 \* 返回此 Optional 适合调试的非空字符串表示形式。
 \*/
    @Override
    public String toString() {
        return value != null
            ? String.format("Optional[%s]", value)
            : "Optional.empty";
    }
}

Stream

新增的Stream API(java.util.stream)将生成环境的函数式编程引入了Java库中。这是目前为止最大的一次对Java库的完善,以便开发者能够写出更加有效、更加简洁和紧凑的代码。
Steam API极大得简化了集合操作(后面我们会看到不止是集合),首先看下这个叫Task的类:

public class Streams  {
    private enum Status {
        OPEN, CLOSED
    };
 
    private static final class Task {
        private final Status status;
        private final Integer points;
 
        Task( final Status status, final Integer points ) {
            this.status = status;
            this.points = points;
        }
 
        public Integer getPoints() {
            return points;
        }
 
        public Status getStatus() {
            return status;
        }
 
        @Override
        public String toString() {
            return String.format( "[%s, %d]", status, points );
        }
    }
}

Task类有一个分数(或伪复杂度)的概念,另外还有两种状态:OPEN或者CLOSED。现在假设有一个task集合:

final Collection< Task > tasks = Arrays.asList(
    new Task( Status.OPEN, 5 ),
    new Task( Status.OPEN, 13 ),
    new Task( Status.CLOSED, 8 ) 
);

首先看一个问题:在这个task集合中一共有多少个OPEN状态的点?
在Java 8之前,要解决这个问题,则需要使用 foreach 循环遍历task集合;
但是在Java 8中可以利用steams解决:包括一系列元素的列表,并且支持顺序和并行处理。

// Calculate total points of all active tasks using sum()
final long totalPointsOfOpenTasks = tasks
    .stream()
    .filter( task -> task.getStatus() == Status.OPEN )
    .mapToInt( Task::getPoints )
    .sum();
 
System.out.println( "Total points: " + totalPointsOfOpenTasks );

运行这个方法的控制台输出是:

Total points: 18

这里有很多知识点值得说。

  • 首先,tasks集合被转换成steam表示;
  • 其次,在steam上的 filter 操作会过滤掉所有CLOSED的task;
  • 第三,mapToInt 操作基于每个task实例的 Task::getPoints 方法将task流转换成Integer集合;
  • 最后,通过sum方法计算总和,得出最后的结果。

在学习下一个例子之前,还需要记住一些steams(点此更多细节)的知识点。Steam之上的操作可分为中间操作和终止操作。详情可参考:Stream API
中间操作会返回一个新的steam——执行一个中间操作(例如 filter )并不会执行实际的过滤操作,而是创建一个新的steam,并将原steam中符合条件的元素放入新创建的steam。
终止操作(例如 forEach 或者 sum ),会遍历steam并得出结果或者附带结果;在执行终止操作之后,steam处理线已经处理完毕,就不能使用了。在几乎所有情况下,终止操作都是立刻对steam进行遍历。
steam的另一个价值是创造性地支持并行处理(parallel processing)。对于上述的tasks集合,我们可以用下面的代码计算所有任务的点数之和:

// Calculate total points of all tasks
final double totalPoints = tasks
   .stream()
   .parallel()
   .map( task -> task.getPoints() ) // or map( Task::getPoints ) 
   .reduce( 0, Integer::sum );
 
System.out.println( "Total points (all tasks): " + totalPoints );

这里我们使用parallel方法并行处理所有的task,并使用reduce方法计算最终的结果。控制台输出如下:

Total points(all tasks): 26.0

对于一个集合,经常需要根据某些条件对其中的元素分组。利用steam提供的API可以很快完成这类任务,代码如下:

// Group tasks by their status
final Map< Status, List< Task > > map = tasks
    .stream()
    .collect( Collectors.groupingBy(Task::getStatus));
System.out.println( map );

控制台的输出如下:

{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}

最后一个关于tasks集合的例子问题是:如何计算集合中每个任务的点数在集合中所占的比重,具体处理的代码如下:

// Calculate the weight of each tasks (as percent of total points) 
final Collection< String > result = tasks
    .stream()                                        // Stream< String >
    .mapToInt( Task::getPoints )                     // IntStream
    .asLongStream()                                  // LongStream
    .mapToDouble( points -> points / totalPoints )   // DoubleStream
    .boxed()                                         // Stream< Double >
    .mapToLong( weigth -> ( long )( weigth \* 100 ) ) // LongStream
    .mapToObj( percentage -> percentage + "%" )      // Stream< String> 
    .collect( Collectors.toList() );                 // List< String > 
 
System.out.println( result );

控制台输出结果如下:

[19%, 50%, 30%]

最后,正如之前所说,Steam API不仅可以作用于Java集合,传统的IO操作(从文件或者网络一行一行得读取数据)可以受益于steam处理,这里有一个小例子:

final Path path = new File( filename ).toPath();
try( Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
    lines.onClose( () -> System.out.println("Done!") ).forEach( System.out::println );
}

Stream的方法 onClose 返回一个等价的有额外句柄的Stream,当Stream的 close() 方法被调用的时候这个句柄会被执行。
Stream API、Lambda表达式还有接口默认方法和静态方法支持的方法引用,是Java 8对软件开发的现代范式的响应。
Stream流与Lambda表达式、方法引用等结合使用,效果还是比较不错的,可以多加练习。

Date/Time API(JSR 310)

Java 8引入了新的Date-Time API(JSR 310)来改进时间、日期的处理。时间和日期的管理一直是最令Java开发者痛苦的问题。java.util.Date和后来的java.util.Calendar一直没有解决这个问题(甚至令开发者更加迷茫)。
因为上面这些原因,诞生了第三方库Joda-Time,可以替代Java的时间管理API。Java 8中新的时间和日期管理API深受Joda-Time影响,并吸收了很多Joda-Time的精华。新的java.time包包含了所有关于日期、时间、时区、Instant(跟日期类似但是精确到纳秒)、duration(持续时间)和时钟操作的类。新设计的API认真考虑了这些类的不变性(从java.util.Calendar吸取的教训),如果某个实例需要修改,则返回一个新的对象。
我们接下来看看java.time包中的关键类和各自的使用例子。首先,Clock 类使用时区来返回当前的纳秒时间和日期。Clock 可以替代 System.currentTimeMillis()TimeZone.getDefault()

// Get the system clock as UTC offset 
final Clock clock = Clock.systemUTC();
System.out.println( clock.instant() );
System.out.println( clock.millis() );

这个例子的输出结果是:

2014-04-12T15:19:29.282Z
1397315969360

第二,关注下 LocalDateLocalTime 类。LocalDate 仅仅包含ISO-8601日历系统中的日期部分;LocalTime则仅仅包含该日历系统中的时间部分。这两个类的对象都可以使用Clock对象构建得到。

// Get the local date and local time
final LocalDate date = LocalDate.now();
final LocalDate dateFromClock = LocalDate.now( clock );
 
System.out.println( date );
System.out.println( dateFromClock );
 
// Get the local date and local time
final LocalTime time = LocalTime.now();
final LocalTime timeFromClock = LocalTime.now( clock );
 
System.out.println( time );
System.out.println( timeFromClock );

上述例子的输出结果如下:

2014-04-12
2014-04-12
11:25:54.568
15:25:54.568

LocalDateTime 类包含了LocalDate和LocalTime的信息,但是不包含ISO-8601日历系统中的时区信息。这里有一些关于LocalDate和LocalTime的例子

// Get the local date/time
final LocalDateTime datetime = LocalDateTime.now();
final LocalDateTime datetimeFromClock = LocalDateTime.now( clock );
 
System.out.println( datetime );
System.out.println( datetimeFromClock );

上述这个例子的输出结果如下:

2014-04-12T11:37:52.309
2014-04-12T15:37:52.309

如果你需要特定时区的data/time信息,则可以使用ZoneDateTime,它保存有ISO-8601日期系统的日期和时间,而且有时区信息。下面是一些使用不同时区的例子:

// Get the zoned date/time
final ZonedDateTime zonedDatetime = ZonedDateTime.now();
final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now( clock );
final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of( "America/Los\_Angeles" ) );
 
System.out.println( zonedDatetime );
System.out.println( zonedDatetimeFromClock );
System.out.println( zonedDatetimeFromZone );

这个例子的输出结果是:

2014-04-12T11:47:01.017-04:00[America/New\_York]
2014-04-12T15:47:01.017Z
2014-04-12T08:47:01.017-07:00[America/Los\_Angeles]

最后看下Duration类,它持有的时间精确到秒和纳秒。这使得我们可以很容易得计算两个日期之间的不同,例子代码如下:

// Get duration between two dates
final LocalDateTime from = LocalDateTime.of( 2014, Month.APRIL, 16, 0, 0, 0 );
final LocalDateTime to = LocalDateTime.of( 2015, Month.APRIL, 16, 23, 59, 59 );
 
final Duration duration = Duration.between( from, to );
System.out.println( "Duration in days: " + duration.toDays() );
System.out.println( "Duration in hours: " + duration.toHours() );

这个例子用于计算2014年4月16日和2015年4月16日之间的天数和小时数,输出结果如下:

Duration in days: 365
Duration in hours: 8783

对于Java 8的新日期时间的总体印象还是比较积极的,一部分是因为Joda-Time的积极影响,另一部分是因为官方终于听取了开发人员的需求。如果希望了解更多细节,可以参考官方文档

Nashorn JavaScript引擎

Java 8提供了新的Nashorn JavaScript引擎,使得我们可以在JVM上开发和运行JS应用。Nashorn JavaScript引擎是javax.script.ScriptEngine的另一个实现版本,这类Script引擎遵循相同的规则,允许Java和JavaScript交互使用,例子代码如下:

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName( "JavaScript" );
System.out.println( engine.getClass().getName() );
System.out.println( "Result:" + engine.eval( "function f() { return 1; }; f() + 1;" ) );

这个代码的输出结果如下:

jdk.nashorn.api.scripting.NashornScriptEngine
Result: 2

Base64

对Base64编码的支持已经被加入到Java 8官方库中,这样不需要使用第三方库就可以进行Base64编码,例子代码如下:

package com.javacodegeeks.java8.base64;
 
import java.nio.charset.StandardCharsets;
import java.util.Base64;
 
public class Base64s {
    public static void main(String[] args) {
        final String text = "Base64 finally in Java 8!";
 
        final String encoded = Base64
            .getEncoder()
            .encodeToString( text.getBytes( StandardCharsets.UTF_8 ) );
        System.out.println( encoded );
 
        final String decoded = new String( 
            Base64.getDecoder().decode( encoded ),
            StandardCharsets.UTF_8 );
        System.out.println( decoded );
    }
}

这个例子的输出结果如下:

QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
Base64 finally in Java 8!

新的Base64API也支持URL和MINE的编码解码。
(Base64.getUrlEncoder() / Base64.getUrlDecoder(), Base64.getMimeEncoder() / Base64.getMimeDecoder())。

并行数组

Java8版本新增了很多新的方法,用于支持并行数组处理。最重要的方法是 parallelSort() ,可以显著加快多核机器上的数组排序。下面的例子论证了parallexXxx 系列的方法:

package com.javacodegeeks.java8.parallel.arrays;
 
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
 
public class ParallelArrays {
    public static void main( String[] args ) {
        long[] arrayOfLong = new long [ 20000 ];        
 
        Arrays.parallelSetAll( arrayOfLong, 
            index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
        Arrays.stream( arrayOfLong ).limit( 10 ).forEach( 
            i -> System.out.print( i + " " ) );
        System.out.println();
 
        Arrays.parallelSort( arrayOfLong );        
        Arrays.stream( arrayOfLong ).limit( 10 ).forEach( 
            i -> System.out.print( i + " " ) );
        System.out.println();
    }
}

上述这些代码使用**parallelSetAll()方法生成20000个随机数,然后使用parallelSort()**方法进行排序。这个程序会输出乱序数组和排序数组的前10个元素。上述例子的代码输出的结果是:

Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378 
Sorted: 39 220 263 268 325 607 655 678 723 793

并发性

基于新增的lambda表达式和steam特性,为Java 8中为java.util.concurrent.ConcurrentHashMap 类添加了新的方法来支持聚焦操作;另外,也为java.util.concurrentForkJoinPool 类添加了新的方法来支持通用线程池操作(更多内容可以参考并发编程)。
Java 8还添加了新的
java.util.concurrent.locks.StampedLock
类,用于支持基于容量的锁——该锁有三个模型用于支持读写操作(可以把这个锁当做是java.util.concurrent.locks.ReadWriteLock 的替代者)。
在**java.util.concurrent.atomic **包中也新增了不少工具类,列举如下:

  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator
  • LongAdder

新的Java工具

Nashorn引擎:jjs

jjs是一个基于标准Nashorn引擎的命令行工具,可以接受js源码并执行。例如,我们写一个func.js文件,内容如下:

function f() { 
     return 1; 
}; 
 
print( f() + 1 );


可以在命令行中执行这个命令:jjs func.js,控制台输出结果是:

2

如果需要了解细节,可以参考官方文档

类依赖分析器:jdeps

jdeps是一个相当棒的命令行工具,它可以展示包层级和类层级的Java类依赖关系,它以**.class**文件、目录或者Jar文件为输入,然后会把依赖关系输出到控制台。
我们可以利用jedps分析下Spring Framework库,为了让结果少一点,仅仅分析一个JAR文件:org.springframework.core-3.0.5.RELEASE.jar。

jdeps org.springframework.core-3.0.5.RELEASE.jar

这个命令会输出很多结果,我们仅看下其中的一部分:依赖关系按照包分组,如果在classpath上找不到依赖,则显示"not found".

org.springframework.core-3.0.5.RELEASE.jar -> C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar
   org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar)
      -> java.io                                            
      -> java.lang                                          
      -> java.lang.annotation                               
      -> java.lang.ref                                      
      -> java.lang.reflect                                  
      -> java.util                                          
      -> java.util.concurrent                               
      -> org.apache.commons.logging                         not found
      -> org.springframework.asm                            not found
      -> org.springframework.asm.commons                    not found
   org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar)
      -> java.lang                                          
      -> java.lang.annotation                               
      -> java.lang.reflect                                  
      -> java.util

更多的细节可以参考官方文档

JVM的新特性

使用 Metaspace (JEP 122)代替持久代(PermGen space)。
在JVM参数方面,使用 -XX:MetaSpaceSize-XX:MaxMetaspaceSize 代替原来的 -XX:PermSize-XX:MaxPermSize

Java 9 新特性

Java9 是Java8后一个比较大的更新,包含新特性比较多,此篇文章只总结下Java 9 版本的一些重要的新特性。并不完全。
Java 9 全部的新特性,请看官网:Java 平台,标准版 Oracle JDK 9 中的新增功能
Java各个版本的文档入口:Java平台,标准版文档
Java各个版本下载:https://jdk.java.net/archive/

模块化

Java 9 中的模块化是对 Java 的一次重大改进。但是模块化并不是最近才提出来的,我们经常使用的 maven 构建工具,就是典型的模块化构建工具。模块化不仅让模块命名清晰,写出高内聚低耦合的代码,更可以方便处理模块之间的调用关系。
module 是新增的Java代码访问权限级别,每个module可以包含多个package。
通过module-info.java文件来声明该文件夹及其子文件夹为一个模块,**exports **关键字可以用来控制模块内哪些包对外暴露。

 module store.api{
   exports com.dingtalk.store.api;
 }

使用module后,即使包中的类是public的,如果未通过exports显式导出其程序包,则外部模块是不可调用的。
如果一个模块想要使用被另一个模块导出的package包中的类,可以用requires关键字在其module-info.java文件中来导入(读取)目标模块的package包。

 module store.service {
   requires com.dingtalk.store.api;
 }

Java9 module 与Maven module 很像,但功能完全不一样,后者是作为依赖构建来方便管理应用代码,而Java Module是在于安全性、访问性控制,通过exports/requires 控制模块内需要暴露和依赖的具体包。

接口支持定义私有方法

在 Java 8 中增加了默认方法,在 Java 9 中又增加了私有方法,这时开始接口中不仅仅有了定义,还具有了行为。我想这是出于代码构造上的考虑,如果没有私有方法,那么当多个默认方法的行为一样时,就要写多个相同的代码。而有了私有方法,事情就变得不一样了。
举个例子:

public class Jdk9Interface {
    public static void main(String[] args) {
        ChinaPeople chinaPeople = new ChinaPeople();
        chinaPeople.sleep();
        chinaPeople.eat();
        chinaPeople.doXxx();
    }

}

class ChinaPeople implements People {
    @Override
    public void sleep() {
        System.out.println("躺着睡");
    }
}

interface People {
    void sleep();

    default void eat() {
        drink();
    }

    default void doXxx() {
        drink();
    }

    private void drink() {
        System.out.println("喝水");
    }
}

例子中的接口 people 中的 eat() 和 doXxx() 默认行为一致,使用私有方法可以方便的抽取一个方法出来。
输出结果:

躺着睡
喝水
喝水

集合工厂方法

在 Java 9 中为集合的创建增加了静态工厂创建方式,也就是 of 方法,通过静态工厂 of 方法创建的集合是只读集合,里面的对象不可改变。并且不能存在 null 值,对于 set 和 map 集合,也不能存在 key 值重复。这样不仅线程安全,而且消耗的内存也更小

// 工厂方法创建集合
List<String> stringList = List.of("a", "b", "c", "d");
Set<String> stringSet = Set.of("a", "b", "c", "d");
Map<String, Integer> stringIntegerMap = Map.of("key1", 1, "key2", 2, "key3", 3);
Map<String, Integer> stringIntegerMap2 = Map.ofEntries(Map.entry("key1", 1), Map.entry("key2", 2));

// 集合输出
System.out.println(stringList);
System.out.println(stringSet);
System.out.println(stringIntegerMap);
System.out.println(stringIntegerMap2);

输出:

[a, b, c, d]
[d, a, c, b]
{key2=2, key1=1, key3=3}
{key2=2, key1=1}

这种只读集合在 Java 9 之前创建是通过 Collections.unmodifiableList 修改集合操作权限实现的。

List<String> arrayList = new ArrayList<>();
arrayList.add("CSDN");
arrayList.add("阿提说说");

// 设置为只读集合
arrayList = Collections.unmodifiableList(arrayList);

静态工厂 of 方法创建的集合还有一个特性,就是工厂内部会自由复用已有实例或者创建新的实例,所以应该避免对 of 创建的集合进行判等或者 haseCode 比较等操作。

// 工厂可以自由创建新的实例或者复用现有实例,所以 使用 of 创建的集合,避免 == 或者 hashCode 判断操作
List<String> stringList = List.of("a", "b", "c", "d");
List<String> stringList2 = List.of("a", "b", "c", "d");
System.out.println(stringList.hashCode());
System.out.println(stringList2.hashCode());
// 输出结果
// 3910595
// 3910595

增强流(Stream)API

Stream 流操作自从 Java 8 引入以来,一直广受好评。
当然,学习 Stream 之前要先学习 Lambda ,也是Java 8的内容。
在 Java 9 中,又对 Stream 进行了增强,主要增加了 4 个新的操作方法:dropWhile,takeWhile,ofNullable,iterate。
1、takeWhile: 从头开始筛选,遇到不满足的就结束

// takeWhile ,从头开始筛选,遇到不满足的就结束了
List<Integer> list1 = List.of(1, 2, 3, 4, 5);
List<Integer> listResult = list1.stream().takeWhile(x -> x < 3).collect(Collectors.toList());
System.out.println(listResult);

// takeWhile ,从头开始筛选,遇到不满足的就结束
List<Integer> list2 = List.of(1, 2, 3, 4, 3, 0);
List<Integer> listResult2 = list2.stream().takeWhile(x -> x < 3).collect(Collectors.toList());
System.out.println(listResult2);

----------------------------------
输出结果:
[1, 2]
[1, 2]

2、dropWhile: 从头开始删除,遇到不满足的就结束

// dropWhile ,从头开始删除,遇到不满足的就结束
List<Integer> list1 = List.of(1, 2, 3, 4, 5);
List<Integer> listResult = list1.stream().dropWhile(x -> x < 3).collect(Collectors.toList());
System.out.println(listResult);

// dropWhile ,从头开始删除,遇到不满足的就结束
List<Integer> list2 = List.of(1, 2, 3, 4, 3, 0);
List<Integer> listResult2 = list2.stream().dropWhile(x -> x < 3).collect(Collectors.toList());
System.out.println(listResult2);

----------------------------------
输出结果:
[3, 4, 5]
[3, 4, 3, 0]

3、ofNullable: 创建支持全 null 的 Stream

Stream<Integer> stream = Stream.of(1, 2, null);
stream.forEach(System.out::print);
System.out.println();

// 空指针异常
// stream = Stream.of(null);
stream = Stream.ofNullable(null);
stream.forEach(System.out::print);

----------------------------------
输出结果:
12null

4、iterate: 可以重载迭代器。

IntStream.iterate(0, x -> x < 10, x -> x + 1).forEach(System.out::print);

----------------------------------
输出结果:
0123456789

在 Stream 增强之外,还增强了 Optional ,Optional 增加了可以转换成 Stream 的方法。

Stream<Integer> s = Optional.of(1).stream();
s.forEach(System.out::print);

HTTP / 2 Client

Java 9 内置了新的 HTTP/2 客户端,请求更加方便。
随便访问一个不存在的网页。

HttpClient client = HttpClient.newHttpClient();
URI uri = URI.create("http://www.baidu.com");
HttpRequest req = HttpRequest.newBuilder(uri).header("User-Agent", "Java").GET().build();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandler.asString());
String body = resp.body();
System.out.println(body);

输出得到的结果,这里是这个网站的报错信息。

There is no method xxxAction in ApiController

Java REPL - JShell

交互式的编程环境在其他语言如 Python 上早就有了,而 Java 上的交互式语言只到 Java 9 才出现。交互式的编程可以让开发者在输入代码的时候就获取到程序的运行结果,而不用像之前一样新建文件、创建类、导包、测试一系列流程。
JShell 中支持 tab 补全代码以及自动添加分号,下面通过一个例子演示 JShell 的使用。
1、进入 JShell. 查看帮助文档

C:\Users>jshell
|  欢迎使用 JShell -- 版本 9
|  要大致了解该版本, 请键入: /help intro
jshell> /help
|  键入 Java 语言表达式, 语句或声明。
|  或者键入以下命令之一:
|  /list [<名称或 id>|-all|-start]
|       列出您键入的源
|  /edit <名称或 id>
|       编辑按名称或 id 引用的源条目
|  /drop <名称或 id>
|       删除按名称或 id 引用的源条目
|  /save [-all|-history|-start] <文件>
|       将片段源保存到文件。
|  /open <file>
|       打开文件作为源输入
|  /vars [<名称或 id>|-all|-start]
|       列出已声明变量及其值
|  /methods [<名称或 id>|-all|-start]
|       列出已声明方法及其签名
|  /types [<名称或 id>|-all|-start]
|       列出已声明的类型
|  /imports
|       列出导入的项
|  /exit
|       退出 jshell
|  /env [-class-path <路径>] [-module-path <路径>] [-add-modules <模块>] ...
|       查看或更改评估上下文
|  /reset [-class-path <路径>] [-module-path <路径>] [-add-modules <模块>]...
|       重启 jshell
|  /reload [-restore] [-quiet] [-class-path <路径>] [-module-path <路径>]...
|       重置和重放相关历史记录 -- 当前历史记录或上一个历史记录 (-restore)
|  /history
|       您键入的内容的历史记录
|  /help [<command>|<subject>]
|       获取 jshell 的相关信息
|  /set editor|start|feedback|mode|prompt|truncation|format ...
|       设置 jshell 配置信息
|  /? [<command>|<subject>]
|       获取 jshell 的相关信息
|  /!
|       重新运行上一个片段
|  /<id>
|       按 id 重新运行片段
|  /-<n>
|       重新运行前面的第 n 个片段
|
|  有关详细信息, 请键入 '/help', 后跟
|  命令或主题的名称。
|  例如 '/help /list' 或 '/help intro'。主题:
|
|  intro
|       jshell 工具的简介
|  shortcuts
|       片段和命令输入提示, 信息访问以及
|       自动代码生成的按键说明
|  context
|       /env /reload 和 /reset 的评估上下文选项

jshell>

2、定义一个变量:a = 10,遍历从 0 到 a 的数字

jshell> int a =10;
a ==> 10
jshell> for(int i=0;i<a;i++){System.out.println(i);}
0
1
2
3
4
5
6
7
8
9

3、定义一个集合,赋值 1,2,3,4,5。然后输出集合

jshell> List list = List.of(1,2,3,4,5);
list ==> [1, 2, 3, 4, 5]
jshell> list
list ==> [1, 2, 3, 4, 5]

4、查看输入过的代码

jshell> /list
   1 : int a =10;
   2 : for(int i=0;i<a;i++){System.out.println(i);}
   3 : List list = List.of(1,2,3,4,5);
   4 : list

5、列出导入的包

jshell> /imports
|    import java.io.\*
|    import java.math.\*
|    import java.net.\*
|    import java.nio.file.\*
|    import java.util.\*
|    import java.util.concurrent.\*
|    import java.util.function.\*
|    import java.util.prefs.\*
|    import java.util.regex.\*
|    import java.util.stream.\*

6、将代码保存到文件并退出

jshell> /save d:/JShell.java
jshell> /exit
  再见

在 D 盘看到的保存的代码片段。

JVM 调优的新特性

第一个:删除 JDK 8 中已弃用的垃圾收集器 (GC) 组合

  • 这意味着以下 GC 组合不再存在:

    • DefNew + CMS
    • ParNew + SerialOld
    • 增量CMS
  • 并发标记扫描 (CMS) 的“前台”模式也已被删除。以下命令行标志已被删除:

    • -Xincgc
    • -XX:+CMSIncrementalMode
    • -XX:+UseCMSCompactAtFullCollection
    • -XX:+CMSFullGCsBeforeCompaction
    • -XX:+UseCMSCollectionPassing
  • 命令行标志-XX:+UseParNewGC不再有效。ParNew 只能与 CMS 一起使用,而 CMS 需要 ParNew。因此,该-XX:+UseParNewGC标志已被弃用,并且可能会在未来的版本中被删除。

第二个:使 G1 成为默认垃圾收集器

  • 使垃圾优先 (G1) 成为 32 位和 64 位服务器配置上的默认垃圾收集器 (GC)。对于大多数用户来说,使用低暂停收集器(例如 G1)比以前默认的面向吞吐量的收集器(例如 Parallel GC)提供更好的整体体验。
  • 增强垃圾优先 (G1) 垃圾收集器以自动确定几个重要的内存回收设置。以前必须手动设置这些设置以获得最佳结果。此外,修复了 G1 垃圾收集器的可用性、确定性和性能问题。
  • 请参阅Java 平台中的 Garbage-First Garbage Collector ,标准版 HotSpot 虚拟机垃圾收集调优指南

第三个:弃用并发标记扫描 (CMS) 垃圾收集器

  • 弃用并发标记扫描 (CMS) 垃圾收集器。-XX:+UseConcMarkSweepGC使用该选项在命令行上请求时会发出警告消息。Garbage-First (G1) 垃圾收集器旨在替代 CMS 的大多数用途。

其他更新

Java 9 中增加或者优化的功能远不止这些,上面只是列举了常用的一些新特性,更多的新特性如:

  • 不能使用下划线 _ 作为变量名,因为它是一个关键字。
  • Javadoc 支持 HTML5 并且支持搜索功能。
  • Nashorn 引擎升级,更好的支持 Javascript.
  • String 存储结构变更从 char -> byte
  • 多Jdk版本共存jar:在同一个Jar包可以包含多个Java版本的class文件,在不同Jdk环境下使用对应该 jdk 版本的 jar。(这对算是用户很友好的功能)

新特性很多,感兴趣的可以自己了解下。
Java 9 全部的新特性,请看官网:[Java 平台,标准版 Oracle JDK 9 中的新增功能](Java 平台,标准版 Oracle JDK 9 中的新增功能)

Java 10 新特性

自从 Java 9 开始,Oracle 调整了 Java 版本的发布策略,不再是之前的 N 年一个大版本,取而代之的是 6 个月一个小版本,三年一个大版本,这样可以让 Java 的最新改变迅速上线,而小版本的维护周期缩短到下个版本发布之前,大版本的维护周期则是 3 年之久。而 10 就是这么一个小版本,因为 Java 的后续版本基本都会包含之前新特性,所以还是把 Java 10 带来的改变单独写一写(不全)。
Java 10 全部的新特性,请看官网:JDK 10 发行说明
Java各个版本的文档入口:Java平台,标准版文档
Java各个版本下载:https://jdk.java.net/archive/

基于时间的版本号

就像上面说的,Java 调整了发布策略,为了适应这种发布节奏,随着改变的还有 Java 版本号的记录方式。
版本号的新模式是: **F

E

A

T

U

R

E

.

FEATURE.

FEATURE.INTERIM.

U

P

D

A

T

E

.

UPDATE.

UPDATE.PATCH**

  • $FEATURE :基于发布版本,如 Java 10 的 10 。
  • $INTERIM :问题修复和功能增强时 + 1,默认是 0 。
  • $UPDATE :在进行兼容更新,修复新功能安全问题时 +1。
  • $PATCH :特殊问题修复时 +1。

查看自己的 Java 10 版本。

$ java -version
java version "10.0.1" 2018-04-17
Java(TM) SE Runtime Environment 18.3 (build 10.0.1+10)
Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10.0.1+10, mixed mode)

局部类型推断

JEP 286 提案让 Java 增加了局部类型推断(Local-Variable Type Inference)功能,这让 Java 可以像 Js 里的 var 或者其他语言的 auto 一样可以自动推断数据类型。这其实只是一个新的语法糖,底层并没有变化,在编译时就已经把 var 转化成具体的数据类型了,但是这样可以减少代码的编写。
你可以像下面这样使用 var 语法。

var hashMap = new HashMap<String, String>();
hashMap.put("CSDN","阿提说说");
var string = "hello java 10";
var stream = Stream.of(1, 2, 3, 4);
var list = new ArrayList<String>();

如果你反编译编译后的这段代码,你会发现还是熟悉的代码片段。

HashMap<String, String> hashMap = new HashMap();
hashMap.put("微信", "wlw");
String string = "hello java 10";
Stream<Integer> stream = Stream.of(1, 2, 3, 4);
ArrayList<String> list = new ArrayList();

var 看似好用,其实也有很多限制,官方介绍了 var 只能用于下面的几种情况。

  1. 仅限带有初始化的程序的局部变量。
  2. for 循环或者增强for 循环中。
  3. for 循环中的声明。
public static void testVar() {
    // 情况1,没有初始化会报错
    // var list;
    var list = List.of(1, 2, 3, 4);
  
    // 情况2
    for (var integer : list) {
        System.out.println(integer);
    }
  
    // 情况3
    for (var i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}

尽管对 var 的使用场景增加了很多限制,但在实际使用时你还是要注意,就像下面的代码,你可能一眼并不能看出 result 的数据类型。

var query = "xxx";
var result = dbUtil.executeQuery(query);

基于 Java 的 JIT 编译器(实验性)

这个功能让基于 Java 开发的 JIT 编译器 **Graal **结合 Java 10 用在 Linux /x64 平台上,这是一个实验性的 JIT 编译器,有人说这也是 Java 10 中最具有未来感的引入。Graal 其实在 Java 9 中就已经引入了,它带来了 Java 中的 AOT (Ahead Of Time)编译,还支持多种语言,如 Js、Python、Ruby、R、以及其他基于 JVM (如 Java、Kotlin)的和基于 LLVM (如 C、C++)的语言。
想切换到 Graal 可以使用下面的 jvm 参数。

-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler

这里面有一点我觉得很有意思,看这个图。
image.png
这就很有意思了,Graal 是 Java 语言编写的,用 Java 编写的编译器,然后用来将 Java 字节码编译机器代码。
Graal 官网:https://www.graalvm.org/(opens new window)

类数据共享

JVM 启动时有一步是需要在内存中加载类,而如果有多个 jar,加载第一个 jar 的速度是最慢的。这就延长了程序的启动时间,为了减少这个时间,Java 10 引入了应用程序类数据共享(CDS)机制,它可以把你想共享的类共享在程序之间,使不同的 Java 进程之间共享这个类来减少这个类占用的空间以及加载速度。

G1并行全GC

早在 Java 9 时就已经引入了 G1 垃圾收集器,G1 的优点很多。而在 Java 10 中还是做了小小调整,当 G1 的并发收集线程不能快速的完成全 GC 时,就会自动切换到并行收集,这可以减少在最坏情况下的 GC 速度。

Unicode 语言标签扩展

这个提案让 JDK 实现了最新的 LDML 规范 (opens new window)中指定的更多的扩展。
主要增加了下面几个扩展方法。

java.time.temporal.WeekFields::of
java.util.Calendar::{getFirstDayOfWeek,getMinimalDaysInWeek}
java.util.Currency::getInstance
java.util.Locale::getDisplayName
java.util.spi.LocaleNameProvider
java.text.DateFormat::get\*Instance
java.text.DateFormatSymbols::getInstance
java.text.DecimalFormatSymbols::getInstance
java.text.NumberFormat::get\*Instance
java.time.format.DateTimeFormatter::localizedBy
java.time.format.DateTimeFormatterBuilder::getLocalizedDateTimePattern
java.time.format.DecimalStyle::of

测试:

Currency chinaCurrency = Currency.getInstance(Locale.CHINA);
Currency usCurrency = Currency.getInstance(Locale.US);
System.out.println("本地货币:" + chinaCurrency);
System.out.println("US.货币:" + usCurrency);

String displayName = Locale.getDefault().getDisplayName();
String displayLanguage = Locale.getDefault().getDisplayLanguage();
String displayCountry = Locale.getDefault().getDisplayCountry();
System.out.println("本地名称:" + displayName);
System.out.println("本地语言:" + displayLanguage);
System.out.println("本地国家:" + displayCountry);
int firstDayOfWeek = Calendar.getInstance().getFirstDayOfWeek();
System.out.println("本地每周第一天:" + firstDayOfWeek);

结果:

本地货币:CNY
US.货币:USD
本地名称:中文 (中国)
本地语言:中文
本地国家:中国
本地每周第一天:1

API更新

Java 10 删除了部分 API,也增加了一些实用方法。比如可以通过 Collection.copyOf 复制得到一个不可改变集合,即使原来的集合元素发生了变化也不会有影响。

var list = new ArrayList<String>();
list.add("wechat");
list.add("wlw");
List<String> copyList = List.copyOf(list);
list.add("test");
System.out.println(copyList);
// result
// [wechat, wn8398]

也为 Optional 增加了一个新的方法 orElseThrow。调用这个方法也可以获取到 optional 中的 value , 但是如果 value 为 null ,就会抛出异常。
另外在 Stream 最后收集数据的时候,Collectors 可以直接指定收集的集合为不可变集合,像下面这样。

list.stream().collect(Collectors.toUnmodifiableList());
list.stream().collect(Collectors.toUnmodifiableSet());

其他更新

Java 10 的更新内容不止这些,上面只是列举了常用的以及比较有意思的新特性。还有部分更新如:
1、JEP 312:Thread-Local Handshakes,JVM 内部功能,可以提高 JVM 性能。
2、JEP 313:删除了 javah 工具,说是删除,其实功能已经包含在 Java 8 中的 javac 里。
3、JEP 316:让 JVM 可以在备用的存储设备(如 NV-DIMM)上分配堆内存,而不用更改程序代码。
4、JEP 319:在 JDK 中提供一组默认的根证书颁发机构(CA)证书。

Java 10 全部的新特性,请看官网:JDK 10 发行说明

Java 11 新特性

Java 11 是 Java 8 之后的第一个 LTS 版本,但是也自从 Java 11 开始, Oracle JDK 不再可以免费的用于商业用途,当然如果你是个人使用,或者是使用 Open JDK ,那么还是可以免费使用的。

Java 11 全部的新特性,请看官网:JDK 11 发行说明
Java各个版本的文档入口:Java平台,标准版文档
Java各个版本下载:https://jdk.java.net/archive/

String API改动

字符串绝对是 Java 中最常用的一个类了,String 类的方法使用率也都非常的高,在 Java 11 中又为 String 类带来了一系列的好用操作。

  1. isBlank()判空
// 判空,blank里我放入了全角空格,半角空格,TAB
String blank = "   ";
System.out.println(blank.isBlank());

// 输出
// true

  1. lines()分割获取字符串流
// lines 返回一个 Stream
String line = "a\nb\nc";
Stream<String> lines = line.lines();

// 使用 lambda 遍历
lines.forEach(System.out::println);

// 输出
// a
// b
// c

  1. repeat()复制字符串
// 复制字符串
String repeat = "CSDN/WX-阿提说说";
String repeat3 = repeat.repeat(3);
System.out.println(repeat3);

// 输出
// CSDN/WX-阿提说说CSDN/WX-阿提说说CSDN/WX-阿提说说

  1. strip()去除前后空白字符
// 去除前后空白
String strip = "   https://www.baidu.com  ";
System.out.println("==" + strip.trim() + "==");
// 去除前后空白字符,如全角空格,TAB
System.out.println("==" + strip.strip() + "==");
// 去前面空白字符,如全角空格,TAB
System.out.println("==" + strip.stripLeading() + "==");
// 去后面空白字符,如全角空格,TAB
System.out.println("==" + strip.stripTrailing() + "==");

// 输出
// ==  https://www.baidu.com  ==
// ==https://www.baidu.com==
// ==https://www.baidu.com  ==
// ==   https://www.baidu.com==

File API改动

读写文件变得更加方便

// 创建临时文件
Path path = Files.writeString(Files.createTempFile("test", ".txt"),  "https://www.baidu.com");

System.out.println(path);
// 读取文件
String s = Files.readString(path);
System.out.println(s);

// 结果
//C:\Users\ADMINI~1\AppData\Local\Temp\test10960104919895436673.txt
//https://www.baidu.com

HTTP Client

在 Java 11 中 Http Client API 得到了标准化的支持。且支持 HTTP/1.1 和 HTTP/2 ,也支持 websockets。
你可以像这样发起一个请求:

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://www.hao123.com"))
        .build();
// 异步
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
        .thenApply(HttpResponse::body)
        .thenAccept(System.out::println)
        .join();

// 同步
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());

更多的如同步异步请求,并发访问,设置代理等方式,可以参考 OpenJDK 官方文档:
http://openjdk.java.net/groups/net/httpclient/recipes-incubating.html(opens new window)

Lambda 局部变量推断

在 Java 10 中引入了 var 语法,可以自动推断变量类型。在 Java 11 中这个语法糖可以在 Lambda 表达式中使用了。

var hashMap = new HashMap<String, Object>();
hashMap.put("CSDN", "阿提说说");
hashMap.put("website", "https://blog.csdn.net/weixin\_40972073");
hashMap.forEach((var k, var v) -> {
    System.out.println(k + ": " + v);
});
//输出
//website: https://blog.csdn.net/weixin\_40972073
//CSDN: 阿提说说

这里需要注意的是,(var k,var v) 中,k 和 v 的类型要么都用 var ,要么都不写,要么都写正确的变量类型。而不能 var 和其他变量类型混用。
image.png

单命令运行Java

自从学习 Java 的第一天,我们就知道运行一个 Java 文件,要先用 javac 命令编译,再用 java 命令运行,而现在只要一个 java 命令就可以运行了。

public class MainTest {
    public static void main(String[] args) {
        System.out.println("CSDN: 阿提说说");
    }
}

//java -Dfile.encoding=UTF-8 MainTest.java
//CSDN: 阿提说说

免费的飞行记录器

商业版 JDK 中一直有一款低开销的事件信息收集工具,也就是飞行记录器(Java Flight Recorder),它可以对 JVM 进行检查、分析、记录等。当出现未知异常时可以通过记录进行故障分析。这个好用的工具在 Java 11 中将开源免费。所有人都可以使用这个功能了。

其他更新

  1. JEP 309 - 添加动态文件常量。
  2. JEP 318 - 添加 Epsilon 垃圾收集器。
  3. JEP 320 - 删除 Java EE 和 corba 模块(java.xml.ws, java.xml.bind, java.activation, java.xml.ws.annotation, java.corba, java.transaction, java.se.ee, jdk.xml.ws, jdk.xml.bind)。
  4. JEP 329 - 增加加密算法 chacha20,poly1305 的实现。
  5. JEP 333 - 引入实验性的 ZGC 垃圾收集器,保证停摆时间不会超过 10ms。
  6. JEP 335 - 废弃 Nashorn JavaScript 引擎

Java 11 全部的新特性,请看官网:JDK 11 发行说明

Java 12 新特性

Java 12 早在 2019 年 3 月 19 日发布,它不是一个长久支持(LTS)版本。此篇文章写一下部分Java 12的新特性。
Java 12 全部的新特性,请看官网:JDK 12 发行说明
Java各个版本的文档入口:Java平台,标准版文档
Java各个版本下载:https://jdk.java.net/archive/

Switch 表达式 改进

在 Java 12 中,对 Switch 表达式的写法进行了改进,虽然是一个语法糖的改进,也让 Switch 的代码编写变得更加优雅。先看一下在 Java 12 之前的 Switch 的写法。
由于 Switch 表达式在 Java 12 中并不是一个正式发布的功能,还处于预览测试阶段,所以想要使用 Java 12 去编译运行就需要打开功能预览参数,当然,如果你使用的是 Java 14 以及更高版本,就可以直接跳过这个部分了。

# 编译时
./bin/javac --enable-preview -source 12 ./Xxx.java
# 运行时
./bin/java --enable-preview Xxx

Java 12以前的switch写法

// 通过传入月份,输出月份所属的季节
public static void switchJava12Before(String day) {
    switch (day) {
        case "march":
        case "april":
        case "may":
            System.out.println("春天");
            break;
        case "june":
        case "july":
        case "august":
            System.out.println("夏天");
            break;
        case "september":
        case "october":
        case "november":
            System.out.println("秋天");
            break;
        case "december":
        case "january":
        case "february":
            System.out.println("冬天");
            break;
    }
}

Java 12的写法

public static void switchJava12(String day) {
    switch (day) {
        case "march", "april", "may"            -> System.out.println("春天");
        case "june", "july", "august"           -> System.out.println("夏天");
        case "september", "october", "november" -> System.out.println("秋天");
        case "december", "january", "february"  -> System.out.println("冬天");
    }
}

另外还可使用返回值赋值

String season = switch (day) {
    case "march", "april", "may"            -> "春天";
    case "june", "july", "august"           -> "夏天";
    case "september", "october", "november" -> "秋天";
    case "december", "january", "february"  -> "冬天";
    default -> {
      //throw new RuntimeException("day error")
        System.out.println("day error");
        break "day error";
    }
};
System.out.println("当前季节是:" + season);

文件对比 Files.mismatch

在 Java 中对于文件的操作已经在 Java 11 中进行了一次增强,这次 Java 12 又带来了文件对比功能。
对比两个文件,如果内容一致,会返回 -1 ,如果内容不同,会返回不同的字节开始位置。

// 创建两个文件
Path pathA = Files.createFile(Paths.get("a.txt"));
Path pathB = Files.createFile(Paths.get("b.txt"));

// 写入相同内容
Files.write(pathA,"abc".getBytes(), StandardOpenOption.WRITE);
Files.write(pathB,"abc".getBytes(), StandardOpenOption.WRITE);
long mismatch = Files.mismatch(pathA, pathB);
System.out.println(mismatch);

// 追加不同内容
Files.write(pathA,"123".getBytes(), StandardOpenOption.APPEND);
Files.write(pathB,"321".getBytes(), StandardOpenOption.APPEND);
mismatch = Files.mismatch(pathA, pathB);
System.out.println(mismatch);

// 删除创建的文件
pathA.toFile().deleteOnExit();
pathB.toFile().deleteOnExit();

// RESULT
// -1
// 3

Compact Number

简化的数字格式可以直接转换数字显示格式,比如 1000 -> 1K,1000000 -> 1M 。

System.out.println("Compact Formatting is:");
NumberFormat upvotes = NumberFormat.getCompactNumberInstance(new Locale("en", "US"), Style.SHORT);

System.out.println(upvotes.format(100));
System.out.println(upvotes.format(1000));
System.out.println(upvotes.format(10000));
System.out.println(upvotes.format(100000));
System.out.println(upvotes.format(1000000));

// 设置小数位数
upvotes.setMaximumFractionDigits(1);
System.out.println(upvotes.format(1234));
System.out.println(upvotes.format(123456));
System.out.println(upvotes.format(12345678));

输出:

100
1K
10K
100K
1M
1.2K
123.5K
12.3M

JVM 相关更新

Shenandoah 垃圾收集器
Java 12 增加了 Shenandoah 一个低停顿的垃圾收集器,它可以和 Java 应用程序中的执行线程同时进行,用来收集垃圾进行内容回收,这样就可以让停顿时间更少。
更多关于 Shenandoah 垃圾收集器的介绍可以查看文档:Shenandoah GC 介绍 (opens new window)。
ZGC 并发类卸载
Z 垃圾收集器现在支持类卸载,通过卸载不使用的类来释放这些类相关的数据结构,从而减少应用程序的总体占用空间。因为是并发执行,所以不会停止 Java 应用程序线程的执行,也因此对 GC 的暂停时间影响微乎其微。默认情况下启用此功能,但可以使用命令行选项禁用** -XX:-ClassUnloading**。
JVM 常量 API
在包** java.lang.invoke.constant** 中定义了一系列的基于值的符号引用,可以用来描述各种可加载常量。可以更容易的对关键类文件和运行时构建的名义描述进行建模,特别是对那些从常量池中加载的常量,也让开发者可以更简单标准的处理可加载常量。
默认使用类数据共享(CDS)
这已经不是 JDK 第一次改进 CDS(Class Data Sharing) 功能了,CDS 可以让 JVM 在同一台机器或虚拟机上启动多个应用的速度速度大大增加。原理是在启动应用时共享一些类加载信息,这样启动新进程时就可以使用共享的数据。在 Java 12 之前此功能需要手动开启,Java 12 调整为默认开启。
微基准套件
Java 12 中添加一套新的基于 JMH 的基本的微基准测试套件。
JMH 的使用,可以参考文章 JMH - Java 代码性能测试的终极利器 (opens new window)。

其他更新

在 Java 11 支持了 Unicode 10 之后, Java 12 支持了 Unicode 11,支持操作更多的表情、符号。

Java 13 新特性

Java 13 早在 2019 年 9 月就已经发布,虽然不是长久支持版本,但是也带来了不少新功能。此篇文章写一下部分Java 13的新特性。
Java 13 全部的新特性,请看官网:JDK 13 发行说明
Java各个版本的文档入口:Java平台,标准版文档
Java各个版本下载:https://jdk.java.net/archive/

Switch 表达式 (二次预览)

在Java 12 中对 Switch 进行了一次增强,这次又对Switch进行了增强。
在 Java 13 中,又对 switch 表达式进行了增强,增加了 yield 关键词用于返回值,相比 break ,语义更加明确了。

public static String switchJava13(String month) {
    return switch (month) {
        case "march", "april", "may":
            yield "春天";
        case "june", "july", "august":
            yield "夏天";
        case "september", "october", "november":
            yield "秋天";
        case "december", "january", "february":
            yield "冬天";
        default:
            yield "month error";
    };
}

动态 CDS 存档

JVM 启动时有一步是需要在内存中加载类,而如果有多个 jar,加载第一个 jar 的速度是最慢的。这就延长了程序的启动时间,为了减少这个时间,Java 10 引入了应用程序类数据共享(CDS)机制,它可以把你想共享的类共享在程序之间,使不同的 Java 进程之间共享这个类来减少这个类占用的空间以及加载速度。不过 Java 10 中使用这个功能的步骤比较繁琐。
而 Java 13 中的 AppCDS,允许 Java 应用在程序执行结束时(如果 JVM 没有崩溃)进行动态存档;存储的内容包括所有加载的应用类型类和使用的类库,这些存储的类库本来并不存在于默认的 CDS 存档中。使用这个功能非常简单,只需要在程序启动时增加启动参数 。

# ArchiveClassesAtExit,程序结束时动态存档
bin/java -XX:ArchiveClassesAtExit=hello.jsa -cp hello.jar Hello
# SharedArchiveFile,使用指定存档启动
bin/java -XX:SharedArchiveFile=hello.jsa -cp hello.jar Hello

ZGC,归还未使用的内存 (实验性)

在 Java 13 之前,ZGC 虽然在清理内存时导致的停顿时间非常少,但是即使内存已经长时间没有使用,ZGC 也不会将内存返还给操作系统,这对那些十分关注内存占用的应用程序非常不友好。
比如:

  • 资源按使用量付费的云上容器环境。
  • 应用虽然长时间闲置,但是占用了内存,导致运行的其他程序内存紧张。

而新增的这个功能,可以让 ZGC 归还长时间没有使用的内存给操作系统,这对某些用户来说十分友好。

重新实现 Socket API

java.net.Socket 和 java.net.ServerSocket 类早在 Java 1.0 时就已经引入了,它们的实现的 Java 代码和 C 语言代码的混合,维护和调试都十分不易;而且这个实现还存在并发问题,有时候排查起来也很困难。
因此,在 Java 13 中引入了新的实现方式,使用了新的实现 NioSocketImpl 来代替老旧的 PlainSocketImpl 实现。虽然功能相同,但是老的方式在当前以及未来几个版本内不会删除,用户随时可以通过 -Djdk.net.usePlainSocketImpl 参数切换回老的实现方式,以兼容意外情况。

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Test {
    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(8000)){
            boolean running = true;
            while(running){
                Socket clientSocket = serverSocket.accept();
                //do something with clientSocket
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用 Java 13 运行,通过参数 -XX:+TraceClassLoading 追踪加载的类,日志中可以看到 NioSocketImpl。

➜  develop ./jdk-13.0.2.jdk/Contents/Home/bin/java -XX:+TraceClassLoading Test.java | grep SocketImpl
[0.699s][info   ][class,load] java.net.SocketImpl source: jrt:/java.base
[0.699s][info   ][class,load] java.net.SocketImpl$$Lambda$173/0x0000000800c37440 source: java.net.SocketImpl
[0.702s][info   ][class,load] sun.net.PlatformSocketImpl source: jrt:/java.base
[0.702s][info   ][class,load] sun.nio.ch.NioSocketImpl source: jrt:/java.base
[0.713s][info   ][class,load] sun.nio.ch.NioSocketImpl$FileDescriptorCloser source: jrt:/java.base

但在 Java 12 并不是 NioSocketImpl

➜  develop ./jdk-12.0.2.jdk/Contents/Home/bin/java -XX:+TraceClassLoading Test.java | grep SocketImpl
[0.665s][info   ][class,load] java.net.SocketImpl source: jrt:/java.base
[0.665s][info   ][class,load] java.net.AbstractPlainSocketImpl source: jrt:/java.base
[0.665s][info   ][class,load] java.net.PlainSocketImpl source: jrt:/java.base
[0.665s][info   ][class,load] java.net.SocksSocketImpl source: jrt:/java.base
[0.666s][info   ][class,load] java.net.AbstractPlainSocketImpl$1 source: jrt:/java.base

文本块 (预览)

在这之前,如果我们把一个 JSON 赋值给字符串:

String content = "{\n"
    + " \"upperSummary\": null,\n"
    + " \"sensitiveTypeList\": null,\n"
    + " \"gmtModified\": \"2022-08-23 10:50:09\",\n"
    + " \"lowerGraph\": null,\n"
    + " \"signature\": \"\",\n"
    + " \"appName\": \"xxx\",\n"
    + " \"lowerSummary\": null,\n"
    + " \"gmtCreate\": \"2022-08-23 10:50:09\",\n"
    + " \"type\": \"CALL\",\n"
    + " \"name\": \"xxxx\",\n"
    + " \"subType\": \"yyy\",\n"
    + " \"id\": 1,\n"
    + " \"projectId\": 1,\n"
    + " \"status\": 1\n"
    + "}";

终于不用写丑陋的长字符串了,从 Java 13 开始你可以使用文本块的方式定义字符串了。

String content2 = """
 {
 "upperSummary": null,
 "sensitiveTypeList": null,
 "gmtModified": "2022-08-23 10:50:09",
 "lowerGraph": null,
 "signature": "",
 "appName": "xxx",
 "lowerSummary": null,
 "gmtCreate": "2022-08-23 10:50:09",
 "type": "CALL",
 "name": "xxxx",
 "subType": "yyy",
 "id": 1,
 "projectId": 1,
 "status": 1
 }
 """;

不过这是一个预览功能,如果你要是在 Java 13 中使用需要手动开启预览功能,这个功能在 Java 15 中正式发布。

Java 14 新特性

Java 14早在 2019 年 9 月就已经发布,虽然不是长久支持版本,但是也带来了不少新功能。此篇文章写一下部分Java 14的新特性。
Java 14全部的新特性,请看官网:JDK 14 发行说明
Java各个版本的文档入口:Java平台,标准版文档
Java各个版本下载:https://jdk.java.net/archive/

instanceof 类型判断(预览)

在 Java 14 之前,使用 instanceof 进行类型判断之后,需要进行对象类型转换后才能使用。

public class Java14BeaforInstanceof {

    public static void main(String[] args) {
        Object obj = new ArrayList<>();
        if (obj instanceof ArrayList) {
            ArrayList list = (ArrayList)obj;
            list.add("www.baidu.com");
        }
        System.out.println(obj);
    }
}


而在 Java 14 中,可以在判断类型时指定变量名称进行类型转换,方便了使用。

public class Java14Instanceof {
    public static void main(String[] args) {
        Object obj = new ArrayList<>();
        if (obj instanceof ArrayList list) {
            list.add("www.baidu.com");
        }
        System.out.println(obj);
    }
}

在使用 instanceof 判断类型成立后,会自动强制转换类型为指定类型。

打包工具(孵化)

在 Java 14 中,引入了打包工具,命令是 jpackage,使用 jpackage 命令可以把 JAR 包打包成不同操作系统支持的软件格式。

jpackage --name myapp --input lib --main-jar main.jar --main-class myapp.Main

常见平台格式如下:

  1. Linux: deb and rpm
  2. macOS: pkg and dmg
  3. Windows: msi and exe

要注意的是,jpackage 不支持交叉编译,也就是说在 windows 平台上是不能打包成 macOS 或者 Linux 系统的软件格式的。

G1 支持 NUMA(非统一内存访问)

G1 收集器现在可以感知 NUMA 内存分配方式,以提高 G1 的性能,可以使用 +XX:+UseNUMA 启用这项功能。
更多阅读文档:https://openjdk.java.net/jeps/345

更有用的 NullPointerExceptions

NullPointerException 一直都是一个比较常见的异常,但是在 Java 14 之前,如果一行有多个表达式时,这时报了空指针后,单纯的从报错信息来看,可能并不知道是哪个对象为 NULL 。

public class Java14NullPointerExceptions {

    public static void main(String[] args) {
        String content1 = "www.baidu.com";
        String content2 = null;
        int length = content1.length() + content2.length();
        System.out.println(length);
    }
}

在 Java 14 之前,从下面的报错中我们只能得到错误出现的行数,但是并不能确定是 conteng1 还是 content2 为 null。

java.lang.NullPointerException
	at other.Other.java14NullPointerExceptions(Other.java:88)

但是在 Java 14 中,会清晰的告诉你 because “content2” is null 。

Records (预览)

record 是一种全新的类型,它本质上是一个 final 类,同时所有的属性都是 final 修饰,它会自动编译出 public get hashcode 、equals、toString 等方法,减少了代码编写量。
示例:编写一个 Dog record 类,定义 name 和 age 属性。

public record Dog(String name, Integer age) {
}


**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注大数据)**
![img](https://img-blog.csdnimg.cn/img_convert/caf2f9fce25306a69ba34aaf25ec0759.png)

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

### instanceof 类型判断(预览)


在 Java 14 之前,使用 instanceof 进行类型判断之后,需要进行对象类型转换后才能使用。



public class Java14BeaforInstanceof {

public static void main(String[] args) {
    Object obj = new ArrayList<>();
    if (obj instanceof ArrayList) {
        ArrayList list = (ArrayList)obj;
        list.add("www.baidu.com");
    }
    System.out.println(obj);
}

}


而在 Java 14 中,可以在判断类型时指定变量名称进行类型转换,方便了使用。



public class Java14Instanceof {
public static void main(String[] args) {
Object obj = new ArrayList<>();
if (obj instanceof ArrayList list) {
list.add(“www.baidu.com”);
}
System.out.println(obj);
}
}


在使用 instanceof 判断类型成立后,会自动强制转换类型为指定类型。


### 打包工具(孵化)


在 Java 14 中,引入了打包工具,命令是 jpackage,使用 jpackage 命令可以把 JAR 包打包成不同操作系统支持的软件格式。



jpackage --name myapp --input lib --main-jar main.jar --main-class myapp.Main


常见平台格式如下:


1. Linux: deb and rpm
2. macOS: pkg and dmg
3. Windows: msi and exe


要注意的是,jpackage 不支持交叉编译,也就是说在 windows 平台上是不能打包成 macOS 或者 Linux 系统的软件格式的。


### G1 支持 NUMA(非统一内存访问)


G1 收集器现在可以感知 NUMA 内存分配方式,以提高 G1 的性能,可以使用 +XX:+UseNUMA 启用这项功能。  
 更多阅读文档:<https://openjdk.java.net/jeps/345>


### 更有用的 NullPointerExceptions


NullPointerException 一直都是一个比较常见的异常,但是在 Java 14 之前,如果一行有多个表达式时,这时报了空指针后,单纯的从报错信息来看,可能并不知道是哪个对象为 NULL 。



public class Java14NullPointerExceptions {

public static void main(String[] args) {
    String content1 = "www.baidu.com";
    String content2 = null;
    int length = content1.length() + content2.length();
    System.out.println(length);
}

}


在 Java 14 之前,从下面的报错中我们只能得到错误出现的行数,但是并不能确定是 conteng1 还是 content2 为 null。



java.lang.NullPointerException
at other.Other.java14NullPointerExceptions(Other.java:88)


但是在 Java 14 中,会清晰的告诉你 because “content2” is null 。


### Records (预览)


record 是一种全新的类型,它本质上是一个 final 类,同时所有的属性都是 final 修饰,它会自动编译出 public get hashcode 、equals、toString 等方法,减少了代码编写量。  
 示例:编写一个 Dog record 类,定义 name 和 age 属性。



public record Dog(String name, Integer age) {
}

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注大数据)
[外链图片转存中…(img-YqbmaFmj-1713406326170)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值