java8新特性

1.Lambda 表达式

Lambda 是一个匿名函数;
->操作符称为Lambda操作符或箭头操作符。它将Lambda 分为两个部分:
左侧:指定 Lambda表达式需要的所有参数
右侧:指定 Lambda 体,即Lambda 表达式要执行的功能。

// 一般匿名类写法
Comparator<Integer> comparator1 = new Comparator<Integer>() {
	@Override
	public int compare(Integer o1, Integer o2) {
		// a negative integer, zero, or a positive integer as the first argument is less
		// than, equal to, or greater than the second.
		return o1 == o2 ? 0 : (o1 > o2 ? 1 : -1);
	}
};
// Lambda 表达式写法
Comparator<Integer> comparator2 = (o1, o2) -> o1 == o2 ? 0 : (o1 > o2 ? 1 : -1);
// 一般匿名类写法
Runnable runnable1 = new Runnable() {
	@Override
	public void run() {
		System.out.println("Hello Runnable1");
	}
};
Thread thread1 = new Thread(runnable1);
thread1.start();
// Lambda 表达式写法
Runnable runnable2 = () -> System.out.println("Hello Runnable2");
Thread thread2 = new Thread(runnable2);
thread2.start();
// Lambda 表达式写法
Thread thread3 = new Thread(() -> System.out.println("Hello Thread"));
thread3.start();
// Lambda 表达式写法
TreeSet<String> set = new TreeSet<String>((p1, p2) -> Integer.compare(p1.length(), p2.length()));
// 格式一:无参数,无返回值
Runnable runnable = () -> System.out.println("Hello Runnable");

// 格式二:
// ①若只有一个参数,并且无返回值;
Consumer<String> consumer1 = (p) -> System.out.println("Hello " + p);
// ②若只有一个参数,小括号可以省略不写
Consumer<String> consumer2 = p -> System.out.println("Hello " + p);

// 格式三:
// ①有两个及以上的参数,且有返回值,且Lambda体中有多条语句
Comparator<Integer> comparator1 = (x, y) -> {
	System.out.println("函数式接口");
	return Integer.compare(x, y);
};
// ②当Lambda体只有一条语句时,return与大括号可以省略
Comparator<Integer> comparator2 = (x, y) -> Integer.compare(x, y);

// 格式四:Lambda表达式的参数列表的数据类型可以省略不写,由JVM编译器通过上下文推断出数据类型,即“类型推断”
// BinaryOperator<Integer> bo1 = (Integer p1, Integer p2) -> p1 + p2;
BinaryOperator<Integer> bo2 = (p1, p2) -> p1 + p2;

2.函数式接口

1)只包含一个抽象方法的接口,称为函数式接口。
2)任意函数式接口上可使用@FunctionalInterface注解检查它是否是一个函数式接口。

2.1 Java内置四大核心函数式接口

函数式接口用途
Consumer<T>T类型对象应用操作。
method: void accept(T t)
Supplier<T>返回T类型对象。
method: T get();
Function<T,R>T类型对象操作,返回R类型对象。
method: R apply(T t);
Predicate<T>T类型对象操作是否满足某约束。
method: boolean test(T t);

其他常用函数式接口

函数式接口用途
BiFunction<T,U,R>对类型为T,U对象操作,返回R类型对象。
method: R apply(T t, U u);
UnaryOperator<T>
(Function子接口)
对类型为T对象进行一元运算,返回T类型对象。
method: T apply(T t);
BinaryOperator<T>
(BiFunction子接口)
对类型为T的对象进行二元运算,并返回T类型的结果。
method: T apply(T t1,T t2);
BiConsumer<T,U>对类型为T,U参数应用操作。
method: void accept(T t, U u)
BiPredicate<T,U>对类型为T,U是否满足某约束。
method: boolean test(T t, U u);

对于基本类型boolean, int, long和double,为避免装箱/拆箱,Java 8提供了一些专门的函数,例如:

函数式接口用途
IntConsumerint类型对象应用操作。
method: void accept(int value)
IntSupplier返回int类型对象。
method: int getAsInt();;
IntPredicateint类型对象操作是否满足某约束。
method: boolean test(int value);
ToIntFunction<T>
ToLongFunction<T>
ToDoubleFunction<T>
分别计算int、long、double、值的函数
int applyAsInt(T value)
long applyAsLong(T value)
IntFunction<R>
LongFunction<R>
DoubleFunction<R>
参数分别为int、long、double类型的函数
R apply(int value)
R apply(long value)

函数式接口也是接口,但只能有一个抽象方法。
Lambda表达式可以赋值给函数式接口:
Comparator<File> comparator = (f1, f2) -> f1.getName().compareTo(f2.getName());

3. 方法引用与构造器引用

3.1 方法引用

:: 方法引用操作符,对象名/类名::方法名
若传递给Lambda体的操作,已经有实现,可以使用方法引用!(实现抽象方法的参数列表,必须与方法引用方法的参数列表保持一致!)
方法引用有如下三种主要使用情况:
1)对象::实例方法
2)类::静态方法
3)类::实例方法

Collection<String> collection = Arrays.asList("中国", "美国", "英国");
// collection.forEach((x) -> System.out.println(x));
collection.forEach(System.out::println);

(或无参)是实例方法的参数时,格式: ClassName::MethodName
// BiPredicate<String, String> bp = (x, y) -> x.equals(y);
BiPredicate<String, String> bp = String::equals;
System.out.println(bp.test("中国", "美国"));
// ①方法引用所引用的方法的参数列表与返回值类型,需要与函数式接口中抽象方法的参数列表和返回值类型保持一致!
// ②若Lambda 的参数列表的第一个参数,是实例方法的调用者,第二个参数
Comparator<Integer> comparator1 = new Comparator<Integer>() {
	@Override
	public int compare(Integer o1, Integer o2) {
		return Integer.compare(o1, o2);
	}
};

Comparator<Integer> comparator2 = (o1, o2) -> Integer.compare(o1, o2);

Comparator<Integer> comparator3 = Integer::compare;

方法引用示例

public class MyStream {
	public static void main(String[] args) {
		// 静态方法 引用
		Supplier<String> supplier1 = Student::solidStr;
		Supplier<String> supplier2 = () -> Student.solidStr();
		System.out.println(supplier1.get());// 哈哈哈,我没有形参哦!
		System.out.println(supplier2.get());// 哈哈哈,我没有形参哦!
		
		// Student::solidStr;
		
		// 实例方法 引用
		Student s1 = new Student("李黑666", 666);
		Student s2 = new Student("李黑777", 777);
		Function<Student, Integer> function1 = Student::getAge;
		Function<Student, Integer> function2 = (Student t) -> t.getAge();
		Integer age1 = function1.apply(s1);
		Integer age2 = function2.apply(s2);
		System.out.printf("age1 = %s \n", age1);// age1 = 666
		System.out.printf("age2 = %s \n", age2);// age2 = 777
		
		Student ss1 = new Student("李黑666", 666);
		Student ss2 = new Student("李黑777", 777);
		BiConsumer<Student, Integer> biConsumer1 = Student::setAge;
		BiConsumer<Student, Integer> biConsumer2 = (t, age) -> t.setAge(age);
		biConsumer1.accept(ss1, 6999);
		biConsumer2.accept(ss2, 7999);
		System.out.printf("ss1.getAge() = %s \n", ss1.getAge());// ss1.getAge() = 6999
		System.out.printf("ss2.getAge() = %s \n", ss2.getAge());// ss2.getAge() = 7999
		
		// 通过变量引用方法
		Student t = new Student("白居易", 666);
		Supplier<String> s11 = t::getName;
		Supplier<String> s12 = () -> t.getName();
		System.out.println("s11.get() = " + s11.get());// s11.get() = 白居易
		System.out.println("s12.get() = " + s12.get());// s12.get() = 白居易
		
		Consumer<String> consumer11 = t::setName;
		Consumer<String> consumer12 = (name) -> t.setName(name);
		consumer11.accept("李太白");
		System.out.println("t.getName() = " + t.getName());// t.getName() = 李太白
		consumer12.accept("青莲居士");
		System.out.println("t.getName() = " + t.getName());// t.getName() = 青莲居士
	}
}



class Student {
	private String name;
	private int age;

	public Student() { }

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

	public String getName() { return name; }

	public void setName(String name) { this.name = name; }

	public int getAge() { return age; }

	public void setAge(int age) { this.age = age; }
	
	public static String solidStr() { return "哈哈哈,我没有形参哦!"; }
	
	public static void solidStr(String val) { System.out.println(val); }

	@Override
	public String toString() { return "Student [name=" + name + ", age=" + age + "]"; }

	@Override
	public int hashCode() { ... }

	@Override
	public boolean equals(Object obj) { ... }
}

3.2 构造器引用 :构造器的参数列表与函数式接口中参数列表保持一致!

类名 :: new
类型[] :: new

// String str = new String("Hello China");
Function<String, String> fun = String::new;
String str = fun.apply("Hello China");
System.out.println(str);
// 定义实体
public class Student {
	private Long id;
	private String name;
	private Integer age;
	private Integer score;
	public Student(Long id, String name, Integer age, Integer score) {
		this.id = id;
		this.name = name;
		this.age = age;
		this.score = score;
	}
	@Override
	public String toString() { return "Student [id=" + id + ", name=" + name + ", age=" + age + ", score=" + score + "]"; }
}
// 定义函数式接口
@FunctionalInterface
public interface FourInputOneOut<A, B, C, D, R> {
	R apply(A a, B b, C c, D d);
}
// 测试类
public class Demo {
	@Test
	public void testConstructorReference() {
		// Student student = new Student(1001L, "李太白", 18, 100);
		FourInputOneOut<Long, String, Integer, Integer, Student> r = Student::new;
		Student student = r.apply(1001L, "李太白", 18, 100);
		System.out.println(student);
	}
}

4.Stream API

4.1 Stream的操作流程

1、创建Stream
一个数据源(如:集合、数组),获取一个流。
2、中间操作
一个中间操作链,对数据源的数据进行处理。
3、终止操作(终端操作)
一个终止操作,执行中间操作链,并产生结果。

4.2 创建Stream(如何生成Stream)

4.2.1 由Collection集合创建Stream

Collection<String> coll = new ArrayList<String>();
Stream<String> stream = coll.stream();// 顺序流
Stream<String> parallelStream = coll.parallelStream();// 并行流

4.2.2 由数组创建流; 使用Arrays的静态方法stream()

Stream<String> stream2 = Arrays.stream(new String[] { "Michael", "Michelle", "William" });

DoubleStream stream3 = Arrays.stream(new double[] { 3, 6, 68 });

//Arrays.stream方法生成的流是数值流【即IntStream】而不是Stream<Integer>。
//数值流可以避免计算过程中拆箱装箱,提高性能。
int[] intArr = new int[]{1, 2, 3, 4, 5};
IntStream stream = Arrays.stream(intArr);

//Stream API提供了mapToInt、mapToDouble、mapToLong三种方式将对象流转换为数值流
//提供了boxed方法将数值流转换为对象流
List<Student> list = new ArrayList<Student>();
IntStream intStream = list.stream().mapToInt(item -> item.getRanking());
Stream<Integer> boxed = intStream.boxed();

4.2.3 由值创建流; 使用静态方法Stream.of()

Stream<String> stream31 = Stream.of("Michael", "Michelle", "William");
Stream<Long> stream32 = Stream.of(1L, 2L, 3L, 4L, 5L);

4.2.4 由函数iterate和generate创建流:创建无限流

=============== iterate ===============
//iterate方法接受两个参数,第一个为初始化值,第二个为函数操作,
//因为iterator生成的流为无限流,通过limit方法对流进行了截断,只生成5个元素
// public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
Stream<String> limit = Stream.iterate("William", new UnaryOperator<String>() {
	@Override
	public String apply(String t) {
		return t;
	}
}).limit(5);
Stream<String> limit = Stream.iterate("William", (t) -> {
	return t;
}).limit(5);


//limit.peek(item -> System.out.println(item)).count();
limit.peek(System.out::println).count();

//limit.forEach(item->System.out.println(item));
limit.forEach(System.out::println);
//limit.forEach(System.out::println); //Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed

=============== generate ===============
//generate方法接受一个参数,方法参数类型为Supplier。
//generate生成的流也是无限流,因此通过limit对流进行了截断
// public static<T> Stream<T> generate(Supplier<T> s)
Stream.generate(new Supplier<String>() {
	@Override
	public String get() {
		return "William";
	}
}).limit(5).forEach(new Consumer<String>() {
	@Override
	public void accept(String t) {
		System.out.println(t);
	}
});


Stream<Double> stream = Stream.generate(Math::random).limit(5);
stream.forEach(System.out::println);

4.2.5 通过文件生成

//import java.nio.charset.Charset;
//import java.nio.file.Files;
//import java.nio.file.Paths;
Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset());

4.3 Stream 的中间操作

多个中间操作可以连接起来,中间操作不会执行任何逻辑,而在终止操作时一次性全部处理,称为“惰性求值”。

筛选与切片

方法描述
filter(Predicate p)接收Lambda ,从流中排除某些元素。
distinct()筛选,通过流所生成元素的hashCode() 和equals() 去除重复元素
limit(long maxSize)截断流,使其元素不超过给定数量。
skip(long n)跳过元素,返回一个扔掉了前n 个元素的流。若流中元素不足n 个,则返回一个空流。与limit(n) 互补
// 老方法
stream.filter(new Predicate<String>() {
	@Override
	public boolean test(String t) {
		return t.startsWith("M");
	}
}).forEach(new Consumer<String>() {
	@Override
	public void accept(String t) {
		System.out.println(t);
	}
});
// Lambda表达式
stream.filter((p) -> p.startsWith("M")).forEach(System.out::println);

map,flatMap映射

方法描述
map(Function f)接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
mapToDouble(ToDoubleFunction f)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的DoubleStream。
mapToInt(ToIntFunction f)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的IntStream。
mapToLong(ToLongFunction f)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的LongStream。
flatMap(Function f)接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流
		
List<String> names1 = list.stream().map(Student::getName).collect(Collectors.toList());
List<String> names2 = list.stream().map((item) -> item.getName()).collect(Collectors.toList());
Set<String> names3 = list.stream().map(Student::getName).collect(Collectors.toSet());

list.stream().forEach((item) -> System.out.println(item));
list.stream().forEach(System.out::println);


List<String> collect = list.stream().flatMap((item) -> {
	return Arrays.stream(item.getName().split(""));
}).collect(Collectors.toList());
System.out.println(collect);//[黎, 明, 1, 李, 白, 2, 李, 煌, 3, 李, 煌, 4, 李, 黑, 5, 李, 黑, 6]

// Creating a list of Strings
List<String> list = Arrays.asList("1", "2", "3", "4", "5");
long sum = list.stream().mapToLong(num -> Long.parseLong(num)).sum();
System.out.println(sum);//15 //即 1+2+3+4+5=15

字符串收集器

String result = Stream.of("我", "爱", "中国").collect(Collectors.joining("$", "<", ">"));
System.out.println(result);

group by分组

class Student {
	private String name;
	private String grade;
	private int age;
}

List<Student> list = new ArrayList<>();
list.add(new Student("黎明1", "11",55));
list.add(new Student("李白2", "11",22));
list.add(new Student("李白2", "22",33));
list.add(new Student("李白2", "33",44));
list.add(new Student("李白2", "22",77));
list.add(new Student("李白2", "11",99));
Map<String, List<Student>> collect1 = list.stream().collect(Collectors.groupingBy(Student::getGrade));
Map<String, List<Student>> collect2 = list.stream().collect(Collectors.groupingBy((item) -> item.getGrade()));
for (Entry<String, List<Student>> entrySet : collect1.entrySet()) {
	System.out.println(entrySet);
}
Map<String, Long> collect3 = list.stream().collect(Collectors.groupingBy(Student::getGrade, Collectors.counting()));
for (Entry<String, Long> entrySet : collect3.entrySet()) {
	System.out.println(entrySet);
}
Map<String, Optional<Student>> collect41 = list.stream().collect(Collectors.groupingBy(Student::getGrade, Collectors.maxBy(Comparator.comparing(Student::getAge))));
for (Entry<String, Optional<Student>> entrySet : collect41.entrySet()) {
	System.out.println(entrySet);
}
System.out.println("========================================");
Map<String, Optional<Student>> collect42 = list.stream().collect(Collectors.groupingBy(Student::getGrade, Collectors.maxBy((item1, item2) -> item1.getAge() - item2.getAge())));
for (Entry<String, Optional<Student>> entrySet : collect42.entrySet()) {
	System.out.println(entrySet);
}
Map<String, Student> collect6 = list.stream().collect(Collectors.groupingBy(Student::getGrade, 
																			Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparing(Student::getAge)),
																										 Optional::get))
													  );
for (Entry<String, Student> entrySet : collect6.entrySet()) {
	System.out.println(entrySet);
}

排除重复数据

# 分组
list.stream().collect(Collectors.groupingBy(Student::getGrade));

# java8有一个collectingAndThen可以根据多个字段去重
list.stream()
.collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(o -> o.getProfessionId() + ";" + o.getGrade()))), ArrayList::new));

#通过hashSet去重:
# 该种去重是bean完全相同的时候算重复数据
List<String> classNameList = new ArrayList(new HashSet(list));

排序

方法描述
sorted()产生一个新流,其中按自然顺序排序
sorted(Comparator comp)产生一个新流,其中按比较器顺序排序

4.4 Stream 的终止操作

终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是void 。

4.4.1 查找与匹配

方法描述
allMatch(Predicate p)检查是否匹配所有元素
anyMatch(Predicate p)检查是否至少匹配一个元素
noneMatch(Predicate p)检查是否没有匹配所有元素
findFirst()返回第一个元素
findAny()返回当前流中的任意元素
count()返回流中元素总数
max(Comparator c)返回流中最大值
min(Comparator c)返回流中最小值
forEach(Consumer c)内部迭代(使用Collection接口需要用户去做迭代,称为外部迭代。相反,Stream API 使用内部迭代—它帮你把迭代做了)

4.4.2 归约

方法描述
reduce(T iden, BinaryOperator b)可以将流中元素反复结合起来,得到一个值。返回T
reduce(BinaryOperator b)可以将流中元素反复结合起来,得到一个值。返回Optional<T>

4.4.3 收集

方法描述
collect(Collector c)将流转换为其他形式。接收一个Collector接口的实现,用于给Stream中元素做汇总的方法
Collector 接口中方法的实现决定了如何对流执行收集操作(如收集到List、Set、Map)。Collectors工具类提供的静态方法可方便地创建常见收集器实例:

5.接口中的默认方法与静态方法

5.1 Interface的默认方法

JDK1.8之后新添加了默认方法,可以定义新的默认方法来对该接口进行扩展,而且不强制要求在实现类中重写该默认方法。

eg:一个类实现了多个接口,且这些接口拥有相同的默认方法签名。

public interface Vehicle {
   default void print(){
      System.out.println("我是一辆车!");
   }
}
 
public interface FourWheeler {
   default void print(){
      System.out.println("我是一辆四轮车!");
   }
}

第一个解决方案是创建自己的默认方法,来覆盖重写接口的默认方法:

public class Car implements Vehicle, FourWheeler {
   default void print(){
      System.out.println("我是一辆四轮汽车!");
   }
}

第二种解决方案可以使用 super 来调用指定接口的默认方法:

public class Car implements Vehicle, FourWheeler {
   public void print(){
      Vehicle.super.print();
   }
}

5.2 Interface的静态方法

Java 8 的另一个特性是接口可以声明(并且可以提供实现)静态方法。

public interface Vehicle {
    // 静态方法
   static void blowHorn(){
      System.out.println("按喇叭!!!");
   }
}

6.新时间日期API

  • LocalDate:表示日期,包含:年月日。格式为:2020-03-04。
  • LocalTime:表示时间,包含:时分秒。格式为:16:18:51.107
  • LocalDateTime:表示日期时间,包含:年月日 时分秒。格式为:2020-03-04T16:18:51.112。
  • DateTimeFormatter:日期时间格式化类。
  • Instant:时间戳类。
  • Duration:用于计算 2 个时间(LocalTime,时分秒)之间的差距。
  • Period:用于计算 2 个日期(LocalDate,年月日)之间的差距。
  • ZonedDateTime:包含时区的时间。
方法描述
now()静态方法,根据当前时间创建对象
of()静态方法,根据指定日期/时间创建对象
plusDays,plusWeeks,plusMonths,plusYears向当前LocalDate对象添加几天、几周、几个月、几年
minusDays,minusWeeks,minusMonths,minusYears从当前LocalDate对象减去几天、几周、几个月、几年
plus,minus添加或减少一个Duration或Period
withDayOfMonth,withDayOfYear,withMonth,withYear将月份天数、年份天数、月份、年份修改为指定的值并返回新的LocalDate对象
getDayOfMonth获得月份天数(1-31)
getDayOfYear获得年份天数(1-366)
getDayOfWeek获得星期几(返回一个DayOfWeek枚举值)
getMonth获得月份,返回一个Month枚举值
getMonthValue获得月份(1-12)
getYear获得年份
until获得两个日期之间的Period对象,或者指定ChronoUnits的数字
isBefore,isAfter比较两个LocalDate
isLeapYear判断是否是闰年
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

LocalDate localDate = LocalDate.now();
LocalTime localTime = LocalTime.now();
LocalDateTime localDateTime=LocalDateTime.now();


LocalDate localDate = LocalDate.of(2016, 10, 26);
LocalTime localTime = LocalTime.of(02, 22, 56);
LocalDateTime localDateTime = LocalDateTime.of(2016, 10, 26, 12, 10, 55);

// java.time.LocalDateTime类型 按指定格式 转为字符串 年月日时分秒毫秒
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")



DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
LocalDate localDate = LocalDate.parse("20210731", dateTimeFormatter);
System.out.println(localDate.toString());//2021-07-31

String localDateStr = localDate.plusDays(1L).format(dateTimeFormatter);
System.out.println(localDateStr);//20210801

Instant 时间戳
用于“时间戳”的运算。它是以Unix元年(传统的设定为UTC时区1970年1月1日午夜时分)开始所经历的描述进行运算

Duration 和Period
Duration:用于计算两个“时间”间隔
Period:用于计算两个“日期”间隔

日期的操纵
TemporalAdjuster : 时间校正器。有时我们可能需要获取例如:将日期调整到“下个周日”等操作。
TemporalAdjusters : 该类通过静态方法提供了大量的常用TemporalAdjuster 的实现。

// 获取下个周日
TemporalAdjuster temporalAdjuster = TemporalAdjusters.next(DayOfWeek.SUNDAY);
LocalDate localDate = LocalDate.now().with(temporalAdjuster);

解析与格式化
java.time.format.DateTimeFormatter 类:该类提供了三种格式化方法:
预定义的标准格式
语言环境相关的格式
自定义的格式

时区的处理
Java8 中加入了对时区的支持,带时区的时间为分别为:
ZonedDate、ZonedTime、ZonedDateTime
其中每个时区都对应着ID,地区ID都为“{区域}/{城市}”的格式
例如:Asia/Shanghai 等
ZoneId:该类中包含了所有的时区信息
getAvailableZoneIds() : 可以获取所有时区时区信息
of(id) : 用指定的时区信息获取ZoneId 对象

7.其他新特性

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值