Java 8——Optional泛型类

整理自http://www.importnew.com/6675.html

           http://www.cnblogs.com/WJ5888/p/4618465.html


一、Optional泛型包装类

Optional类的目的是表示被Optional泛型包装的类型有可能为空(null),并让对这种情况作出处理。

新版本的Java 8引入了一个新的Optional类。Optional类的Javadoc描述如下:

这是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。

本文会逐个探讨Optional类包含的方法,并通过一两个示例展示如何使用。

of

为非null的值创建一个Optional。

of方法通过工厂方法创建Optional类。需要注意的是,创建对象时传入的参数不能为null。如果传入参数为null,则抛出NullPointerException 。

1
2
3
4
//调用工厂方法创建Optional实例
Optional<String> name = Optional.of( "Sanaulla" );
//传入参数为null,抛出NullPointerException.
Optional<String> someNull = Optional.of( null );

ofNullable

为指定的值创建一个Optional,如果指定的值为null,则返回一个空的Optional。

ofNullable与of方法相似,唯一的区别是可以接受参数为null的情况。示例如下:

1
2
3
//下面创建了一个不包含任何值的Optional实例
//例如,值为'null'
Optional empty = Optional.ofNullable( null );

isPresent

非常容易理解

如果值存在返回true,否则返回false。

类似下面的代码:

1
2
3
4
5
//isPresent方法用来检查Optional实例中是否包含值
if (name.isPresent()) {
   //在Optional实例内调用get()返回已存在的值
   System.out.println(name.get()); //输出Sanaulla
}

get

如果Optional有值则将其返回,否则抛出NoSuchElementException。

上面的示例中,get方法用来得到Optional实例中的值。下面我们看一个抛出NoSuchElementException的例子:

1
2
3
4
5
6
7
//执行下面的代码会输出:No value present
try {
   //在空的Optional实例上调用get(),抛出NoSuchElementException
   System.out.println(empty.get());
} catch (NoSuchElementException ex) {
   System.out.println(ex.getMessage());
}

ifPresent

如果Optional实例有值则为其调用consumer,否则不做处理

要理解ifPresent方法,首先需要了解Consumer类。简答地说,Consumer类包含一个抽象方法。该抽象方法对传入的值进行处理,但没有返回值。Java8支持不用接口直接通过lambda表达式传入参数。

如果Optional实例有值,调用ifPresent()可以接受接口段或lambda表达式。类似下面的代码:

1
2
3
4
5
//ifPresent方法接受lambda表达式作为参数。
//lambda表达式对Optional的值调用consumer进行处理。
name.ifPresent((value) -> {
   System.out.println( "The length of the value is: " + value.length());
});

orElse

如果有值则将其返回,否则返回指定的其它值。

如果Optional实例有值则将其返回,否则返回orElse方法传入的参数。示例如下:

1
2
3
4
5
6
//如果值不为null,orElse方法返回Optional实例的值。
//如果为null,返回传入的消息。
//输出:There is no value present!
System.out.println(empty.orElse( "There is no value present!" ));
//输出:Sanaulla
System.out.println(name.orElse( "There is some value!" ));

orElseGet

orElseGet与orElse方法类似,区别在于得到的默认值。orElse方法将传入的字符串作为默认值,orElseGet方法可以接受Supplier接口的实现用来生成默认值。示例如下:

1
2
3
4
5
6
//orElseGet与orElse方法类似,区别在于orElse传入的是默认值,
//orElseGet可以接受一个lambda表达式生成默认值。
//输出:Default Value
System.out.println(empty.orElseGet(() -> "Default Value" ));
//输出:Sanaulla
System.out.println(name.orElseGet(() -> "Default Value" ));

orElseThrow

如果有值则将其返回,否则抛出supplier接口创建的异常。

在orElseGet方法中,我们传入一个Supplier接口。然而,在orElseThrow中我们可以传入一个lambda表达式或方法,如果值不存在来抛出异常。示例如下:

1
2
3
4
5
6
7
8
9
try {
   //orElseThrow与orElse方法类似。与返回默认值不同,
   //orElseThrow会抛出lambda表达式或方法生成的异常
 
   empty.orElseThrow(ValueAbsentException:: new );
} catch (Throwable ex) {
   //输出: No value present in the Optional instance
   System.out.println(ex.getMessage());
}

ValueAbsentException定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ValueAbsentException extends Throwable {
 
   public ValueAbsentException() {
     super ();
   }
 
   public ValueAbsentException(String msg) {
     super (msg);
   }
 
   @Override
   public String getMessage() {
     return "No value present in the Optional instance" ;
   }
}

map

map方法文档说明如下:

如果有值,则对其执行调用mapping函数得到返回值。如果返回值不为null,则创建包含mapping返回值的Optional作为map方法返回值,否则返回空Optional。

map方法用来对Optional实例的值执行一系列操作。通过一组实现了Function接口的lambda表达式传入操作。如果你不熟悉Function接口,可以参考我的这篇博客。map方法示例如下:

1
2
3
4
//map方法执行传入的lambda表达式参数对Optional实例的值进行修改。
//为lambda表达式的返回值创建新的Optional实例作为map方法的返回值。
Optional<String> upperName = name.map((value) -> value.toUpperCase());
System.out.println(upperName.orElse( "No value found" ));

flatMap

如果有值,为其执行mapping函数返回Optional类型返回值,否则返回空Optional。flatMap与map(Funtion)方法类似,区别在于flatMap中的mapper返回值必须是Optional。调用结束时,flatMap不会对结果用Optional封装。

flatMap方法与map方法类似,区别在于mapping函数的返回值不同。map方法的mapping函数返回值可以是任何类型T,而flatMap方法的mapping函数必须是Optional。

参照map函数,使用flatMap重写的示例如下:

1
2
3
4
5
//flatMap与map(Function)非常类似,区别在于传入方法的lambda表达式的返回类型。
//map方法中的lambda表达式返回值可以是任意类型,在map函数返回之前会包装为Optional。
//但flatMap方法中的lambda表达式返回值必须是Optionl实例。
upperName = name.flatMap((value) -> Optional.of(value.toUpperCase()));
System.out.println(upperName.orElse( "No value found" )); //输出SANAULLA

filter

filter个方法通过传入限定条件对Optional实例的值进行过滤。文档描述如下:

如果有值并且满足断言条件返回包含该值的Optional,否则返回空Optional。

读到这里,可能你已经知道如何为filter方法传入一段代码。是的,这里可以传入一个lambda表达式。对于filter函数我们应该传入实现了Predicate接口的lambda表达式。如果你不熟悉Predicate接口,可以参考这篇文章

现在我来看看filter的各种用法,下面的示例介绍了满足限定条件和不满足两种情况:

1
2
3
4
5
6
7
8
9
10
//filter方法检查给定的Option值是否满足某些条件。
//如果满足则返回同一个Option实例,否则返回空Optional。
Optional<String> longName = name.filter((value) -> value.length() > 6 );
System.out.println(longName.orElse( "The name is less than 6 characters" )); //输出Sanaulla
 
//另一个例子是Optional值不满足filter指定的条件。
Optional<String> anotherName = Optional.of( "Sana" );
Optional<String> shortName = anotherName.filter((value) -> value.length() > 6 );
//输出:name长度不足6字符
System.out.println(shortName.orElse( "The name is less than 6 characters" ));

以上,我们介绍了Optional类的各个方法。下面通过一个完整的示例对用法集中展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class OptionalDemo {
 
   public static void main(String[] args) {
     //创建Optional实例,也可以通过方法返回值得到。
     Optional<String> name = Optional.of( "Sanaulla" );
 
     //创建没有值的Optional实例,例如值为'null'
     Optional empty = Optional.ofNullable( null );
 
     //isPresent方法用来检查Optional实例是否有值。
     if (name.isPresent()) {
       //调用get()返回Optional值。
       System.out.println(name.get());
     }
 
     try {
       //在Optional实例上调用get()抛出NoSuchElementException。
       System.out.println(empty.get());
     } catch (NoSuchElementException ex) {
       System.out.println(ex.getMessage());
     }
 
     //ifPresent方法接受lambda表达式参数。
     //如果Optional值不为空,lambda表达式会处理并在其上执行操作。
     name.ifPresent((value) -> {
       System.out.println( "The length of the value is: " + value.length());
     });
 
     //如果有值orElse方法会返回Optional实例,否则返回传入的错误信息。
     System.out.println(empty.orElse( "There is no value present!" ));
     System.out.println(name.orElse( "There is some value!" ));
 
     //orElseGet与orElse类似,区别在于传入的默认值。
     //orElseGet接受lambda表达式生成默认值。
     System.out.println(empty.orElseGet(() -> "Default Value" ));
     System.out.println(name.orElseGet(() -> "Default Value" ));
 
     try {
       //orElseThrow与orElse方法类似,区别在于返回值。
       //orElseThrow抛出由传入的lambda表达式/方法生成异常。
       empty.orElseThrow(ValueAbsentException:: new );
     } catch (Throwable ex) {
       System.out.println(ex.getMessage());
     }
 
     //map方法通过传入的lambda表达式修改Optonal实例默认值。
     //lambda表达式返回值会包装为Optional实例。
     Optional<String> upperName = name.map((value) -> value.toUpperCase());
     System.out.println(upperName.orElse( "No value found" ));
 
     //flatMap与map(Funtion)非常相似,区别在于lambda表达式的返回值。
     //map方法的lambda表达式返回值可以是任何类型,但是返回值会包装成Optional实例。
     //但是flatMap方法的lambda返回值总是Optional类型。
     upperName = name.flatMap((value) -> Optional.of(value.toUpperCase()));
     System.out.println(upperName.orElse( "No value found" ));
 
     //filter方法检查Optiona值是否满足给定条件。
     //如果满足返回Optional实例值,否则返回空Optional。
     Optional<String> longName = name.filter((value) -> value.length() > 6 );
     System.out.println(longName.orElse( "The name is less than 6 characters" ));
 
     //另一个示例,Optional值不满足给定条件。
     Optional<String> anotherName = Optional.of( "Sana" );
     Optional<String> shortName = anotherName.filter((value) -> value.length() > 6 );
     System.out.println(shortName.orElse( "The name is less than 6 characters" ));
 
   }
 
}

上述代码输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
Sanaulla
No value present
The length of the value is: 8
There is no value present!
Sanaulla
Default Value
Sanaulla
No value present in the Optional instance
SANAULLA
SANAULLA
Sanaulla
The name is less than 6 characters


二、Lambda表达式

Lambda表达式的目的是:

Java 8中每一个Lambda表达式必须有一个函数式接口与之对应。

利用函数式的写法对parameters执行expression中的操作,其整体是一个函数式接口的对象,parameters为函数式接口唯一抽象方法的形参,且类型可以省略,编译器自动识别类型,expression为函数式接口唯一抽象方法的内部实现。也常用来代替匿名内部类的冗杂语法。

1.1 引言

课本上说编程有两种模式,面向过程的编程以及面向对象的编程,其实在面向对象编程之前还出现了面向函数的编程(函数式编程,以前一直被忽略、不被重视,现在从学术界已经走向了商业界,对函数编程语言的支持目前有ScalaErlangF#PythonPhpJavaJavascript等,有人说他将会是编程语言中的下一个主流...

1.2 Lambda表达式

为什么需要Lambda表达式?

1.使用Lambda表达式可以使代码变的更加紧凑,例如在Java中实现一个线程,只输出一个字符串Hello World!,我们的代码如下所示:

复制代码
public static void main(String[] args) throws Exception {
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("Hello World!");
        }
    }).start();
    TimeUnit.SECONDS.sleep(1000);
}
复制代码

使用Lambda表达式之后代码变成如下形式:

public static void main(String[] args) throws Exception {
    new Thread(() -> System.out.println("Hello World!")).start();
    TimeUnit.SECONDS.sleep(1000);
}

是不是代码变的更紧凑了~,其他的例如各种监听器,以及事件处理器等都可以用这种方式进行简化。

2.修改方法的能力,其实说白了,就是函数中可以接受以函数为单元的参数,在C/C++中就是函数指针,在Java中就是Lambda表达式,例如在Java中使用集合类对一个字符串按字典序列进行排序,代码如下所示:

public static void main(String[] args) {
    String []datas = new String[] {"peng","zhao","li"};
    Arrays.sort(datas);
    Stream.of(datas).forEach(param ->     System.out.println(param));
}

在上面代码中用了Arrays里的sort方法,现在我们不需要按字典排序,而是按字符串的长度进行排序,代码如下所示:

public static void main(String[] args) {
    String []datas = new String[] {"peng","zhao","li"};
    Arrays.sort(datas,(v1 , v2) -> Integer.compare(v1.length(), v2.length()));
    Stream.of(datas).forEach(param -> System.out.println(param));
}

是不是很方便,我们不需要实现Comparable接口,使用一个Lambda表达式就可以改变一个函数的形为~

1.3 Syntax

1.Lambda表达式的形式化表示如下所示

Parameters -> an expression 

2.如果Lambda表达式中要执行多个语句块,需要将多个语句块以{}进行包装,如果有返回值,需要显示指定return语句,如下所示:

Parameters -> {expressions;};

3.如果Lambda表达式不需要参数,可以使用一个空括号表示,如下示例所示

() -> {for (int i = 0; i < 1000; i++) doSomething();};

4.Java是一个强类型的语言,因此参数必须要有类型,如果编译器能够推测出Lambda表达式的参数类型,则不需要我们显示的进行指定,如下所示,在Java中推测Lambda表达式的参数类型与推测泛型类型的方法基本类似,至于Java是如何处理泛型的,此处略去

String []datas = new String[] {"peng","zhao","li"};
Arrays.sort(datas,(String v1, String v2) -> Integer.compare(v1.length(), v2.length()));

上述代码中 显示指定了参数类型Stirng,其实不指定,如下代码所示,也是可以的,因为编译器会根据Lambda表达式对应的函数式接口Comparator<String>进行自动推断

String []datas = new String[] {"peng","zhao","li"};;
Arrays.sort(datas,(v1, v2) -> Integer.compare(v1.length(), v2.length()));

5.如果Lambda表达式只有一个参数,并且参数的类型是可以由编译器推断出来的,则可以如下所示使用Lambda表达式,即可以省略参数的类型及括号

Stream.of(datas).forEach(param -> {System.out.println(param.length());});

6.Lambda表达式的返回类型,无需指定,编译器会自行推断,说是自行推断

7.Lambda表达式的参数可以使用修饰符及注解,如final@NonNull等 

1.4函数式接口

函数式接口是Java 8为支持Lambda表达式新发明的,在上面讲述的Lambda Syntax时提到的sort排序方法就是一个样例,在这个排序方法中就使用了一个函数式接口,函数的原型声明如下所示

public static <T> void sort(T[] a, Comparator<? super T> c)

上面代码中Comparator<? Super T>就是一个函数式接口,? Super T or ? entends TJava 5支持泛型时开始引入,得理解清楚,在此忽略讲述

什么是函数式接口

1.函数式接口具有两个主要特征,是一个接口,这个接口具有唯一的一个抽像方法,我们将满足这两个特性的接口称为函数式接口,说到这,就不得不说一下接口中是有具体实现这个问题啦~

2.Lambda表达式不能脱离目标类型存在,这个目录类型就是函数式接口,所下所示是一个样例

String []datas = new String[] {"peng","zhao","li"};
Comparator<String> comp = (v1,v2) -> Integer.compare(v1.length(), v2.length());
Arrays.sort(datas,comp);
Stream.of(datas).forEach(param -> {System.out.println(param);}); 

Lambda表达式被赋值给了comp函数接口变量

3.函数式接口可以使用@FunctionalInterface进行标注,使用这个标注后,主要有两个优势,编译器知道这是一个函数式接口,符合函数式的要求,另一个就是生成Java Doc时会进行显式标注

4.异常,如果Lambda表达式会抛出非运行时异常,则函数式接口也需要抛出异常,说白了,还是一句话,函数式接口是Lambda表达式的目标类型

5.函数式接口中可以定义public static方法,想想在Java中我们提供了Collection接口,同时还提供了一个Collections工具类等等,在Java中将这种Collections的实现转移到了接口里面,但是为了保证向后兼容性,以前的这种Collection/Collections等逻辑均未改变

6.函数式接口可以提供多个抽像方法,纳尼!上面不是说只能有一个嘛?是的,在函数式接口中可以提供多个抽像方法,但这些抽像方法限制了范围,只能是Object类型里的已有方法,为什么要这样做呢?此处忽略,大家可以自已研究

7.函数式接口里面可以定义方法的默认实现,如下所示是Predicate类的代码,不仅可以提供一个default实现,而且可以提供多个default实现呢,Java 8以前可以嘛?我和我的小伙伴们都惊呆了,这也就导致出现了多继承下的问题,想知道Java 8是如何对其进行处理的嘛,其实很Easy,后面我会再讲~

8.为什么要提供default接口的实现?如下就是一个默认实现

default Predicate<T> or(Predicate<? super T> other) {
     Objects.requireNonNull(other);
     return (t) -> test(t) || other.test(t);
}

Java 8中在接口中增加了默认实现这种函数,其实在很大程序上违背了接口具有抽象这种特征的,增加default实现主要原因是因为考虑兼容及代码的更改成本,例如,在Java 8中向iterator这种接口增加一个方法,那么实现这个接口的所有类都要需实现一遍这个方法,那么Java 8需要更改的类就太多的,因此在Iterator接口里增加一个default实现,那么实现这个接口的所有类就都具有了这种实现,说白了,就是一个模板设计模式吧

1.5方法引用 

有时,我们需要执行的代码在某些类中已经存在,这时我们没必要再去写Lambda表达式,可以直接使用该方法,这种情况我们称之为方法引用,如下所示,未采用方法引用前的代码

如下所示

Stream.of(datas).forEach(param -> {System.out.println(param);});

使用方法引用后的代码如下所示

Stream.of(datas).forEach(System.out::println);

以上示例使用的是out对象,下面示例使用的是类的静态方法引用对字符串数组里的元素忽略大小写进行排序

String []datas = new String[] {"peng","Zhao","li"};
Arrays.sort(datas,String::compareToIgnoreCase);
Stream.of(datas).forEach(System.out::println);

上面就是方法引用的一些典型示例

方法引用的具体分类

Object:instanceMethod
Class:staticMethod
Class:instanceMethod

上面分类中前两种在Lambda表达式的意义上等同,都是将参数传递给方法,如上示例

System.out::println == x -> System.out.println(x)

最后一种分类,第一个参数是方法执行的目标,如下示例 

String::compareToIgnoreCase == (x,y) ->     x.compareToIgnoreCase(y)

还有类似于super::instanceMethod这种方法引用本质上与Object::instanceMethod类似

1.6构造方法引用

构造方法引用与方法引用类似,除了一点,就是构造方法引用的方法是new!以下是两个示例

示例一:

String str = "test";
Stream.of(str).map(String::new).peek(System.out::println).findFirst();

示例二:

String []copyDatas = Stream.of(datas).toArray(String[]::new);
Stream.of(copyDatas).forEach(x -> System.out.println(x));

总结一下,构造方法引用有两种形式

Class::new
Class[]::new

1.7 Lambda表达式作用域

总体来说,Lambda表达式的变量作用域与内部类非常相似,只是条件相对来说,放宽了些以前内部类要想引用外部类的变量,必须像下面这样

复制代码
final String[] datas = new String[] { "peng", "Zhao", "li" };
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(datas);
    }
}).start();
复制代码

将变量声明为final类型的,现在在Java 8中可以这样写代码

复制代码
String []datas = new String[] {"peng","Zhao","li"};
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(datas);
    }
}).start();
复制代码

也可以这样写

new Thread(() -> System.out.println(datas)).start();

总之你爱怎么写,就怎么写吧,I don’t Care it!

看了上面的两段代码,能够发现一个显著的不同,就是Java 8中内部类或者Lambda表达式对外部类变量的引用条件放松了,不要求强制的加上final关键字了,但是Java 8中要求这个变量是effectively final

What is effectively final?

Effectively final就是有效只读变量,意思是这个变量可以不加final关键字,但是这个变量必须是只读变量,即一旦定义后,在后面就不能再随意修改,如下代码会编译出错

String []datas = new String[] {"peng","Zhao","li"};
datas = null;
new Thread(() -> System.out.println(datas)).start();

Java中内部类以及Lambda表达式中也不允许修改外部类中的变量,这是为了避免多线程情况下的race condition

Lambda中变量以及this关键字

Lambda中定义的变量与外部类中的变量作用域相同,即外部类中定义了,Lambda就不能再重复定义了,同时在Lambda表达式使用的this关键字,指向的是外部类,大家可以自行实践下,此处略

未完待写....


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 好的,下面是一个简单的泛型类Java 代码示例: ``` public class MyGenericClass<T> { private T myVar; public MyGenericClass(T myVar) { this.myVar = myVar; } public T getMyVar() { return myVar; } public void setMyVar(T myVar) { this.myVar = myVar; } } ``` 在这个示例中,我们定义了一个泛型类 `MyGenericClass`,它可以接受任何类型的参数。在这个类中,我们定义了一个私有变量 `myVar`,类型为泛型参数 `T`。我们还实现了两个公共方法 `getMyVar` 和 `setMyVar`,用于获取和设置 `myVar` 的值。 泛型类可以让我们编写更加通用的代码,可以适用于不同类型的数据。当我们需要创建一个特定类型的对象时,只需要将该类型作为泛型类的参数即可。 ### 回答2: 泛型类是指在类的定义中使用了一个或多个类型参数,这样我们就可以在创建对象的时候指定具体的类型。在Java中,我们使用关键字<T>来表示类型参数。 下面是一个简单的例子,展示了如何使用Java编写一个泛型类: ```java public class GenericClass<T> { private T data; public GenericClass(T data) { this.data = data; } public T getData() { return data; } public void setData(T data) { this.data = data; } public static void main(String[] args) { GenericClass<Integer> integerObj = new GenericClass<>(10); System.out.println("Integer data: " + integerObj.getData()); GenericClass<String> stringObj = new GenericClass<>("Hello"); System.out.println("String data: " + stringObj.getData()); } } ``` 在上述代码中,我们定义了一个泛型类`GenericClass<T>`,使用类型参数`<T>`表示类可以接受任意类型的数据。我们使用`data`成员变量来存储泛型类型的数据,在构造函数中初始化,并通过`getData()`和`setData()`方法对其进行访问和修改。 在`main`方法中,我们创建了两个`GenericClass`对象:一个接收整数类型,另一个接收字符串类型。然后通过`getData()`方法获取和打印对象中的数据。 这个例子只是展示了泛型类的基本用法,泛型类还有更多高级用法,比如限制类型参数的范围,通配符等。在实际开发中,我们可以根据需要使用不同的泛型类来提高代码的可复用性和灵活性。 ### 回答3: 泛型类是使用一个或多个类型参数作为其属性或方法的数据类型的类。在Java中,我们可以使用泛型类来增加代码的灵活性和重用性。下面是一个使用Java编写的简单泛型类示例: ```java public class GenericClass<T> { private T data; public GenericClass(T data) { this.data = data; } public T getData() { return data; } public void setData(T data) { this.data = data; } public static void main(String[] args) { // 创建整型泛型类对象 GenericClass<Integer> intObj = new GenericClass<>(10); System.out.println("整型数据为: " + intObj.getData()); // 创建字符串泛型类对象 GenericClass<String> stringObj = new GenericClass<>("Hello, World!"); System.out.println("字符串为: " + stringObj.getData()); } } ``` 在上述代码中,我们定义了一个泛型类`GenericClass<T>`,其中`T`是一个类型参数。我们在类的构造函数和属性中使用类型参数`T`。在`main`方法中,我们创建了一个整型和一个字符串型的泛型类对象,并分别打印出其数据。 通过使用泛型类,我们能够在不同的类中使用相同的代码,只需改变类型参数即可适应不同的数据类型。这大大提高了代码的可重用性和灵活性。 该示例只是一个简单的用法示例,实际上,泛型类可以应用于更复杂的情况,例如集合类、容器类等,以实现更高级的功能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值