【Java面试突击-1】Java基础(上)

基本类型

Java中有8种基本类型:

  • 6种数字类型:
    • 整型类型:byte short int long
    • 浮点类型:float double
  • 字符类型:char
  • 布尔类型:boolean

这 8 种基本数据类型的默认值以及所占空间的大小如下:

基本类型位数字节默认值取值范围
byte810-128 ~ 127
short1620-32768 ~ 32767
int3240-2147483648 ~ 2147483647
long6480L-9223372036854775808 ~ 9223372036854775807
char162‘u0000’0 ~ 65535
float3240f1.4E-45 ~ 3.4028235E38
double6480d4.9E-324 ~ 1.7976931348623157E308
boolean1falsetrue, false

对于 boolean,官方文档未明确定义,它依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1 位

Java 的每种基本类型所占存储空间的大小不会像其他大多数语言那样随机器硬件架构的变化而变化。这种所占存储空间大小的不变性是 Java 程序比用其他大多数语言编写的程序更具可移植性的原因之一(《Java 编程思想》2.2 节有提到)。

和 C++ 一样,Java 提供了基本数据类型,这种数据的变量不需要使用 new 创建,他们不会在堆上创建,而是直接在栈内存中存储,因此会更加高效。

注意:

  1. Java 里使用 long 类型的数据一定要在数值后面加上 L,否则将作为整型解析。
  2. char a = 'h' char :单引号,String a = "hello" :双引号。
  3. 这八种基本类型都有对应的包装类分别为:ByteShortIntegerLongFloatDoubleCharacterBoolean

基本类型和包装类型

Java 语言是一个面向对象的语言,但是 Java 中的基本数据类型却是不面向对象的,这在实际使用时存在很多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)。

包装类均位于 java.lang 包,包装类和基本数据类型的对应关系如下表所示

基本数据类型包装类
byteByte
booleanBoolean
shortShort
charCharacter
intInteger
longLong
floatFloat
doubleDouble

自动装箱与自动拆箱

在 Java SE5 之后,为了减少开发人员的工作,Java 提供了自动拆箱与自动装箱功能。

自动装箱: 就是将基本数据类型自动转换成对应的包装类。

自动拆箱:就是将包装类自动转换成对应的基本数据类型。

    Integer i = 5;  //自动装箱
    int b = i;     //自动拆箱

Integer i=5 可以替代 Integer i = new Integer(5);,这就是因为 Java 帮我们提供了自动装箱的功能,不需要开发者手动去 new 一个 Integer 对象。

自动装箱与自动拆箱的实现原理

我们来看下具体实现原理,Java是如何实现自动拆装箱的。

    public static  void main(String[]args){
        Integer integer=1; //装箱
        int i=integer; //拆箱
    }

对上面代码进行泛编译后

    public static  void main(String[]args){
        Integer integer=Integer.valueOf(1);
        int i=integer.intValue();
    }

从上面反编译后的代码可以看出,int 的自动装箱都是通过 Integer.valueOf() 方法来实现的,Integer 的自动拆箱都是通过 integer.intValue 来实现的。

结论:

自动装箱都是通过包装类的 valueOf() 方法来实现的.自动拆箱都是通过包装类对象的 xxxValue() 来实现的。

八种类型都valueOf()和xxxValue()说明见后续内容。

哪些场景会自动装箱和拆箱

1,初始化和赋值

很简单一开始的介绍

2,函数参数与返回值

如上面的介绍和下面:

//自动拆箱
public int getNum1(Integer num) {
 return num;
}
//自动装箱
public Integer getNum2(int num) {
 return num;
}

3,基本数据放入集合中

    List<Integer> li = new ArrayList<>();
    for (int i = 1; i < 50; i ++){
    	//可以正常放入,执行
        li.add(i);
    }

反编译代码

    List<Integer> li = new ArrayList<>();
    for (int i = 1; i < 50; i += 2){
        li.add(Integer.valueOf(i));
    }

4,包装类型与基本类型进行比较

直接使用 Integer包装类型 与 基本类型进行比较。

    Integer a = 1;
    System.out.println(a == 1 ? "等于" : "不等于");
    Boolean bool = false;
    System.out.println(bool ? "真" : "假");

反编译代码

    Integer a = 1;
    System.out.println(a.intValue() == 1 ? "等于" : "不等于");
    Boolean bool = false;
    System.out.println(bool.booleanValue() ? "真" : "假");

结论,包装类与基本数据类型进行比较运算,是先将包装类进行拆箱成基本数据类型,然后进行比较的。

5,三目运算符的使用

看一个例子,你可能会看出一个问题出来

boolean flag = true; //设置成true,保证条件表达式的表达式二一定可以执行
boolean simpleBoolean = false; //定义一个基本数据类型的boolean变量
Boolean nullBoolean = null;//定义一个包装类对象类型的Boolean变量,值为null

boolean x = flag ? nullBoolean : simpleBoolean; //使用三目运算符并给x变量赋值

有经验的同学,是否发现这里会出现NPE。

我们反编译上面的代码得到:

boolean flag = true;
boolean simpleBoolean = false;
Boolean nullBoolean = null;
boolean x = flag ? nullBoolean.booleanValue() : simpleBoolean;

可以看到,反编译后的代码的最后一行,编译器帮我们做了一次自动拆箱,而就是因为这次自动拆箱,导致代码出现对于一个null对象(nullBoolean.booleanValue())的调用,导致了NPE。

这里直接给出结论:
Java定义:只要表达式1和表达式2的类型有一个是基本类型,就会做触发类型对齐的拆箱操作,如果都是包装对象并且返回值也是包装对象则不处理。
最后看下可能的情况:

boolean flag = true;
boolean simpleBoolean = false;
Boolean objectBoolean = Boolean.FALSE;

//当第二位和第三位表达式都是对象时,表达式返回值也为对象;
Boolean x1 = flag ? objectBoolean : objectBoolean; 
//反编译后代码为:Boolean x1 = flag ? objectBoolean : objectBoolean; 
//因为x1的类型是对象,所以不需要做任何特殊操作。

//当第二位和第三位表达式都为基本类型时,表达式返回值也为基本类型;
boolean x2 = flag ? simpleBoolean : simpleBoolean; 
//反编译后代码为:boolean x2 = flag ? simpleBoolean : simpleBoolean;
//因为x2的类型也是基本类型,所以不需要做任何特殊操作。

//当第二位和第三位表达式中有一个为基本类型时,表达式返回值也为基本类型;
boolean x3 = flag ? objectBoolean : simpleBoolean; 
//反编译后代码为:boolean x3 = flag ? objectBoolean.booleanValue() : simpleBoolean;
//因为x3的类型是基本类型,所以需要对其中的包装类进行拆箱。

//当第二位和第三位表达式都是对象时,表达式返回值也为对象;
boolean x4 = flag ? objectBoolean : objectBoolean; 
//反编译后代码为:boolean x4 = (flag ? objectBoolean : objectBoolean).booleanValue();
//因为x4的类型是基本类型,所以需要对表达式结果进行自动拆箱。

//当第二位和第三位表达式都为基本类型时,表达式返回值也为基本类型;
Boolean x5 = flag ? simpleBoolean : simpleBoolean; 
//反编译后代码为:Boolean x5 = Boolean.valueOf(flag ? simpleBoolean : simpleBoolean);
//因为x5的类型是对象类型,所以需要对表达式结果进行自动装箱。

//当第二位和第三位表达式中有一个为基本类型时,表达式返回值也为基本类型;
Boolean x6 = flag ? objectBoolean : simpleBoolean;  
//反编译后代码为:Boolean x6 = Boolean.valueOf(flag ? objectBoolean.booleanValue() : simpleBoolean);
//因为x6的类型是对象类型,所以需要对表达式结果进行自动装箱。

自动装拆箱的缓存机制

我们先来看下一个例子,猜一下运行结果:

package com.javapapers.java;

public class JavaIntegerCache {
    public static void main(String... strings) {

        Integer integer1 = 3;
        Integer integer2 = 3;

        if (integer1 == integer2)
            System.out.println("integer1 == integer2");
        else
            System.out.println("integer1 != integer2");

        Integer integer3 = 300;
        Integer integer4 = 300;

        if (integer3 == integer4)
            System.out.println("integer3 == integer4");
        else
            System.out.println("integer3 != integer4");

    }
}

根据基本的知识我们可能判断上面2个片段都为false。因为在Java中,==比较的是对象应用,而equals比较的是值。所以,在这个例子中,不同的对象有不同的引用,所以在进行比较的时候都将返回false。但是实际运行结果为:

integer1 == integer2
integer3 != integer4

这说明了 Integer 操作下,返回了同一个Integer对象。

原因是在Java 5中,在Integer的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用。

适用于整数值区间-128 至 +127。
只适用于自动装箱。使用构造函数创建对象不适用。

源码内容:

/**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value.  If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
     * to yield significantly better space and time performance by
     * caching frequently requested values.
     *
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
/**
     * Cache to support the object identity semantics of autoboxing for values between
     * -128 and 127 (inclusive) as required by JLS.
     *
     * The cache is initialized on first usage.  The size of the cache
     * may be controlled by the {@code -XX:AutoBoxCacheMax=} option.
     * During VM initialization, java.lang.Integer.IntegerCache.high property
     * may be set and saved in the private system properties in the
     * sun.misc.VM class.
     */

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

结论:当需要主动装箱的时候,如果数字在-128到127之间时,会直接使用缓存中的对象,而不是重新创建一个对象。

实际上这个功能在Java 5中引入的时候,范围是固定的-128 至 +127。后来在Java 6中,可以通过java.lang.Integer.IntegerCache.high设置最大值。这使我们可以根据应用程序的实际情况灵活地调整来提高性能。
到底是什么原因选择这个-128到127范围呢?因为这个范围的数字是最被广泛使用的。
在程序中,第一次使用Integer的时候也需要一定的额外时间来初始化这个缓存。

这种缓存行为不仅适用于Integer对象。我们针对所有的整数类型的类都有类似的缓存机制。

注意 Float,Double 无缓存支持,都是new一个新的。
其他类型的装箱操作

//Short
public static Short valueOf(short s) {
 final int offset = 128;
 int sAsInt = s;
 if (sAsInt >= -128 && sAsInt <= 127) { // must cache
 return ShortCache.cache[sAsInt + offset];
 }
 return new Short(s);
}
//Byte
public static Byte valueOf(byte b) {
 final int offset = 128;
 return ByteCache.cache[(int)b + offset];
}
//Character
public static Character valueOf(char c) {
 if (c <= 127) { // must cache
 return CharacterCache.cache[(int)c];
 }
 return new Character(c);
}
//Long
public static Long valueOf(long l) {
 final int offset = 128;
 if (l >= -128 && l <= 127) { // will cache
 return LongCache.cache[(int)l + offset];
 }
 return new Long(l);
}
//Boolean
public static Boolean valueOf(boolean b) {
 //public static final Boolean TRUE = new Boolean(true);
 //public static final Boolean FALSE = new Boolean(false);
 return (b ? TRUE : FALSE);
}
//Float
public static Float valueOf(float f) {
 return new Float(f);
}
//Double
public static Double valueOf(double d) {
 return new Double(d);
}

自动装箱与拆箱带来的问题

虽然自动装拆箱功能大大节省了开发人员的精力,隐藏细节,但同时也引入问题

1,上面说到的 缓存问题,会导致通过 Integer装箱后的对象比较在 -128到127之间可以使用==完成比较,范围之外就失效。(再一次说明值比较的时候还是要用equals
2,在三目运算的场景下,容易发生NPE情况。
3,内存浪费。尤其在for循环中,避免发生装拆箱场景,导致生成大量无用对象。

字符串

字符串的不可变性

结论:大家都知道字符串是不可变性的,这里是这么理解的?我们来看下分析过程,比如下面代码;

String s = "abcd";
s = s.concat("ef");

这里虽然看起来改变了String s; 但实际上,我们已经得到一个新字符串了
在这里插入图片描述
如上图,在堆中重新创建了一个"abcdef"字符串,和"abcd"并不是同一个对象。

所以,一旦一个string对象在内存(堆)中被创建出来,他就无法被修改。而且,String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。

如果我们想要一个可修改的字符串,可以选择StringBuffer 或者 StringBuilder这两个代替String。

怎么做到不可变的?

1,保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
2,String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

注意Java8 与 Java 9 的实现区别 char 变成了 byte


//Java 8
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];
	//...
}
//Java9
public final class String implements java.io.Serializable,Comparable<String>, CharSequence {
    // @Stable 注解表示变量最多被修改一次,称为“稳定的”。
    @Stable
    private final byte[] value;
}

//Java9
abstract class AbstractStringBuilder implements Appendable, CharSequence {
    byte[] value;

}

原因:

新版的 String 其实支持两个编码方案: Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。Latin-1 编码方案下,byte 占一个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间。

JDK 官方就说了绝大部分字符串对象只包含 Latin-1 可表示的字符。

String设计为不可变的好处

结论:从缓存,安全性,线程安全,性能来考虑

1,缓存
字符串大量使用,创建字符串都是消耗资源的,所以Java需要设计一个缓存功能,对相同的字符串缓存起来,这里就是 JVM中专门开辟的一个空间来存储Java字符串的,叫字符串常量池。
通过字符串池,两个内容相同的字符串变量,可以之乡同一个字符串对象,节省了空间

String s = "abcd";
String s2 = s;

如例子 s 和 s2 都表示 “abcd”,所以都会指向字符串池中同一个字符串对象。

2,安全性
String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。

3,线程安全
不可变会自动使字符串成为线程安全的,因为当从多个线程访问它们时,它们不会被更改。

4,性能
因为字符串被广泛的应用在 HashMap、HashTable、HashSet中等。在对这些散列实现进行操作时,经常调用hashCode()方法。因为String的不可变性,所以只需要计算一次hash,不担心字符串被改变后的实效。

字符串拼接

我们已经知道了字符串是一个不可变的类,所以一旦被实例化就无法被修改。
所以所有的字符串拼接,都是重新生成了一个新的字符串。下面一段字符串拼接代码:

String s = "abcd";
s = s.concat("ef");

其实最后我们得到的s已经是一个新的字符串了。如下图
在这里插入图片描述

字符串拼接方式

使用“+”拼接字符串
String wechat = "Hollis";
String introduce = "每日更新Java相关技术文章";
String hollis = wechat + "," + introduce;

使用concat
String wechat = "Hollis";
String introduce = "每日更新Java相关技术文章";
String hollis = wechat.concat(",").concat(introduce);

使用StringBuffer

关于字符串,Java中除了定义了一个可以用来定义字符串常量的String类以外,还提供了可以用来定义字符串变量的StringBuffer类,它的对象是可以扩充和修改的。

StringBuffer wechat = new StringBuffer("Hollis");
String introduce = "每日更新Java相关技术文章";
StringBuffer hollis = wechat.append(",").append(introduce);

使用StringBuilder

除了StringBuffer以外,还有一个类StringBuilder也可以使用,其用法和StringBuffer类似。如:

StringBuilder wechat = new StringBuilder("Hollis");
String introduce = "每日更新Java相关技术文章";
StringBuilder hollis = wechat.append(",").append(introduce);

使用StringUtils.join

除了JDK中内置的字符串拼接方法,还可以使用一些开源类库中提供的字符串拼接方法名,如apache.commons中提供的StringUtils类,其中的join方法可以拼接字符串。

String wechat = "Hollis";
String introduce = "每日更新Java相关技术文章";
System.out.println(StringUtils.join(wechat, ",", introduce));

StringUtils中提供的join方法,最主要的功能是:将数组或集合以某拼接符拼接到一起形成新的字符串,如:

String []list  ={"Hollis","每日更新Java相关技术文章"};
String result= StringUtils.join(list,",");
System.out.println(result);
//结果:Hollis,每日更新Java相关技术文章

并且,Java8中的String类中也提供了一个静态的join方法,用法和StringUtils.join类似。

String对“+” 的实现

Java中,想要拼接字符串,最简单的方式就是通过"+"连接两个字符串。

有人把Java中使用+拼接字符串的功能理解为运算符重载。其实并不是,Java是不支持运算符重载的。这其实只是Java提供的一个语法糖。

看下这段代码,看下Java时如何实现的

String wechat = "Hollis";
String introduce = "每日更新Java相关技术文章";
String hollis = wechat + "," + introduce;

反编译下代码

String wechat = "Hollis";
String introduce = "\u6BCF\u65E5\u66F4\u65B0Java\u76F8\u5173\u6280\u672F\u6587\u7AE0";//每日更新Java相关技术文章
String hollis = (new StringBuilder()).append(wechat).append(",").append(introduce).toString();

通过查看反编译以后的代码,我们可以发现,原来字符串常量在拼接过程中,是将String转成了StringBuilder后,使用其append方法进行处理的。

那么也就是说,Java中的+对字符串的拼接,其实现原理是使用StringBuilder.append。

但是,String的使用+字符串拼接也不全都是基于StringBuilder.append,还有种特殊情况,那就是如果是两个固定的字面量拼接,如:

String s = "a" + "b"

编译器会进行常量折叠(因为两个都是编译期常量,编译期可知),直接变成 String s = “ab”。

因为“+”会导致 new StringBuilder创建对象,所以在循环中如“for”不要简单的使用 “+” 去拼接字符串。

这也是阿里巴巴Java开发手册建议:循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。而不要使用”+“。

字符串拼接原理实现

concat

我们再来看一下concat方法的源代码,看一下这个方法又是如何实现的。

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

这段代码首先创建了一个字符数组,长度是已有字符串和待拼接字符串的长度之和,再把两个字符串的值复制到新的字符数组中,并使用这个字符数组创建一个新的String对象并返回。

通过源码我们也可以看到,经过concat方法,其实是new了一个新的String,这也印证了字符串的不变性。

StringBuffer和StringBuilder

和String类类似,StringBuilder类也封装了一个字符数组,定义如下:

char[] value;

与String不同的是,它并不是final的,所以他是可以修改的。另外,与String不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:

int count;

其append源码如下:

public StringBuilder append(String str) {
    super.append(str);
    return this;
}

该类继承了AbstractStringBuilder类,看下其append方法:

public AbstractStringBuilder append(String str) {
    if (str == null)
        return appendNull();
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}

append会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩展。

StringBuffer和StringBuilder类似,最大的区别就是StringBuffer是线程安全的,看一下StringBuffer的append方法。

public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

可以看出来,这里使用synchronized进行声明,说明是一个线程安全的方法。而StringBuilder则不是线程安全的。

StringUtils.join

通过查看StringUtils.join的源代码,我们可以发现,其实他也是通过StringBuilder来实现的。

public static String join(final Object[] array, String separator, final int startIndex, final int endIndex) {
    if (array == null) {
        return null;
    }
    if (separator == null) {
        separator = EMPTY;
    }

    // endIndex - startIndex &gt; 0:   Len = NofStrings *(len(firstString) + len(separator))
    //           (Assuming that all Strings are roughly equally long)
    final int noOfItems = endIndex - startIndex;
    if (noOfItems &lt;= 0) {
        return EMPTY;
    }

    final StringBuilder buf = new StringBuilder(noOfItems * 16);

    for (int i = startIndex; i &lt; endIndex; i++) {
        if (i &gt; startIndex) {
            buf.append(separator);
        }
        if (array[i] != null) {
            buf.append(array[i]);
        }
    }
    return buf.toString();
}

效率比较

循环比较的代码:

long t1 = System.currentTimeMillis();
//这里是初始字符串定义
for (int i = 0; i &lt; 50000; i++) {
    //这里是字符串拼接代码
}
long t2 = System.currentTimeMillis();
System.out.println("cost:" + (t2 - t1));

结论 StringBuilder < StringBuffer < concat < + < StringUtils.join

StringBuffer在StringBuilder的基础上,做了同步处理,所以在耗时上会相对多一些。

StringUtils.join也是使用了StringBuilder,并且其中还是有很多其他操作,所以耗时较长,这个也容易理解。其实StringUtils.join更擅长处理字符串数组或者列表的拼接。

字符串常量池

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true

HotSpot 虚拟机中字符串常量池的实现是 src/hotspot/share/classfile/stringTable.cpp ,StringTable 本质上就是一个HashSet ,容量为 StringTableSize(可以通过 -XX:StringTableSize 参数来设置)。

StringTable 中保存的是字符串对象的引用,字符串对象的引用指向堆中的字符串对象。

JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。

为什么移动的原因:
主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存

new String(“abc”)

String s1 = new String(“abc”);这句话创建了几个字符串对象?

1,如果字符串常量池中不存在字符串对象“abc”的引用,那么会在堆中创建 2 个字符串对象“abc”。

String s1 = new String("abc");这句话创建了几个字符串对象?

2、如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。

// 字符串常量池中已存在字符串对象“abc”的引用
String s1 = "abc";
// 下面这段代码只会在堆中创建 1 个字符串对象“abc”
String s2 = new String("abc");

Object

Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:

/**
 * native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
 */
public final native Class<?> getClass()
/**
 * native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
 */
public native int hashCode()
/**
 * 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
 */
public boolean equals(Object obj)
/**
 * naitive 方法,用于创建并返回当前对象的一份拷贝。
 */
protected native Object clone() throws CloneNotSupportedException
/**
 * 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
 */
public String toString()
/**
 * native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
 */
public final native void notify()
/**
 * native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
 */
public final native void notifyAll()
/**
 * native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
 */
public final native void wait(long timeout) throws InterruptedException
/**
 * 多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 毫秒。。
 */
public final void wait(long timeout, int nanos) throws InterruptedException
/**
 * 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
 */
public final void wait() throws InterruptedException
/**
 * 实例被垃圾回收器回收的时候触发的操作
 */
protected void finalize() throws Throwable { }

== 和 equals() 的区别

== 对于基本类型和引用类型的作用效果是不同的:

对于基本数据类型来说,== 比较的是值。
对于引用数据类型来说,== 比较的是对象的内存地址。

equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。

Object 类 equals() 方法:

public boolean equals(Object obj) {
     return (this == obj);
}

equals() 方法存在两种使用情况:

1,类没有重写 equals()方法 :通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object类equals()方法。

2,类重写了 equals()方法 :一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。

String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
System.out.println(aa == bb);// true
System.out.println(a == b);// false
System.out.println(a.equals(b));// true
System.out.println(42 == 42.0);// true

String 中的 equals 方法是被重写过的,因为 Object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。

当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

hashCode() 有什么用?

hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。

hashCode()定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashCode() 方法是本地方法,也就是用 C 语言或 C++ 实现的,该方法通常用来将对象的内存地址转换为整数之后返回。

public native int hashCode();

散列表存储的是键值对(key-value),这其中就利用到了散列码!(可以快速找到所需要的对象)

为什么重写 equals() 时必须重写 hashCode() 方法?

因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。

如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。

总结 :
equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
两个对象有相同的 hashCode 值,他们也不一定是相等的(哈希碰撞)。

Java关键字

transient

我们发现ArrayList类和Vector类都是使用数组实现的,但是在定义数组elementData这个属性时稍有不同,那就是ArrayList使用transient关键字。

  /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

Java语言的关键字,变量修饰符,如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。这里的对象存储是指,Java的serialization提供的一种持久化对象实例的机制。当一个对象被序列化的时候,transient型变量的值不包括在序列化的表示中,然而非transient型的变量是被包括进去的。

instanceof

instanceof 是 Java 的一个二元操作符,类似于 ==,>,< 等操作符。

instanceof 是 Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。

以下实例创建了 displayObjectClass() 方法来演示 Java instanceof 关键字用法:

public static void displayObjectClass(Object o) {
  if (o instanceof Vector)
     System.out.println("对象是 java.util.Vector 类的实例");
  else if (o instanceof ArrayList)
     System.out.println("对象是 java.util.ArrayList 类的实例");
  else
    System.out.println("对象是 " + o.getClass() + " 类的实例");

final

final是Java中的一个关键字,它所表示的是“这部分是无法修改的”。

使用 final 可以定义 :变量、方法、类。


//如果将变量设置为final,则不能更改final变量的值(它将是常量)。
//一旦final变量被定义之后,是无法进行修改的。
class Test{
     final String name = "Hollis";
 
}


//如果任何方法声明为final,则不能覆盖它。
//当我们定义以上类的子类的时候,无法覆盖其name方法,会编译失败。
class Parent {
    final void name() {
        System.out.println("Hollis");
    }
}



//如果把任何一个类声明为final,则不能继承它。
//次类不能被继承
final class Parent {
    
}

static

static表示“静态”的意思,用来修饰成员变量和成员方法,也可以形成静态static代码块

1,静态变量
我们用static表示变量的级别,一个类中的静态变量,不属于类的对象或者实例。因为静态变量与所有的对象实例共享,因此他们不具线程安全性。

通常,静态变量常用final关键来修饰,表示通用资源或可以被所有的对象所使用。如果静态变量未被私有化,可以用“类名.变量名”的方式来使用。

//static variable example
private static int count;
public static String str;

2,静态方法
与静态变量一样,静态方法是属于类而不是实例。
一个静态方法只能使用静态变量和调用静态方法。通常静态方法通常用于想给其他的类使用而不需要创建实例。例如:Collections class(类集合)。

//static util method
public static int addInts(int i, int...js){
    int sum=i;
    for(int x : js) sum+=x;
    return sum;
}

从Java8以上版本开始也可以有接口类型的静态方法了。

3,静态代码块

Java的静态块是一组指令在类装载的时候在内存中由Java ClassLoader执行。

静态块常用于初始化类的静态变量。大多时候还用于在类装载时候创建静态资源。

Java不允许在静态块中使用非静态变量。一个类中可以有多个静态块。静态块只在类装载入内存时,执行一次。


static {
        //can be used to initialize resources when class is loaded
        System.out.println("StaticExample static block");
        //can access only static variables and methods
        str="Test";
        setCount(2);
}

4,静态类
Java可以嵌套使用静态类,但是静态类不能用于嵌套的顶层。

静态嵌套类的使用与其他顶层类一样,嵌套只是为了便于项目打包。

静态类使用完整样例:

package com.journaldev.misc;

public class StaticExample {

    //static block
    static{
        //can be used to initialize resources when class is loaded
        System.out.println(&quot;StaticExample static block&quot;);
        //can access only static variables and methods
        str=&quot;Test&quot;;
        setCount(2);
    }
    
    //multiple static blocks in same class
    static{
        System.out.println(&quot;StaticExample static block2&quot;);
    }
    
    //static variable example
    private static int count; //kept private to control it's value through setter
    public static String str;
    
    public int getCount() {
        return count;
    }

    //static method example
    public static void setCount(int count) {
        if(count &gt; 0)
        StaticExample.count = count;
    }
    
    //static util method
    public static int addInts(int i, int...js){
        int sum=i;
        for(int x : js) sum+=x;
        return sum;
    }

    //static class example - used for packaging convenience only
    public static class MyStaticClass{
        public int count;
        
    }

}

枚举

背景

为什么Java要设计枚举出来,设想下面的场景;

在java语言中还没有引入枚举类型之前,表示枚举类型的常用模式是声明一组具有int常量。之前我们通常利用public final static 方法定义的代码如下,分别用1 表示春天,2表示夏天,3表示秋天,4表示冬天。

public class Season {
    public static final int SPRING = 1;
    public static final int SUMMER = 2;
    public static final int AUTUMN = 3;
    public static final int WINTER = 4;
}

这种方法称作int枚举模式。可这种模式有什么问题呢,我们都用了那么久了,应该没问题的。
通常我们写出来的代码都会考虑它的安全性、易用性和可读性。
这里安全性就达不到,如果要求传入春夏秋冬的某个值。但是使用int类型,我们无法保证传入的值为合法。代码如下所示:

private String getChineseSeason(int season){
        StringBuffer result = new StringBuffer();
        switch(season){
            case Season.SPRING :
                result.append("春天");
                break;
            case Season.SUMMER :
                result.append("夏天");
                break;
            case Season.AUTUMN :
                result.append("秋天");
                break;
            case Season.WINTER :
                result.append("冬天");
                break;
            default :
                result.append("地球没有的季节");
                break;
        }
        return result.toString();
    }

    public void doSomething(){
        System.out.println(this.getChineseSeason(Season.SPRING));//这是正常的场景

        System.out.println(this.getChineseSeason(5));//这个却是不正常的场景,这就导致了类型不安全问题
    }

程序getChineseSeason(Season.SPRING)是我们预期的使用方法。可getChineseSeason(5)显然就不是了,而且编译会通过,但是在运行时会出现各种无法预料到的情况,使用起来细节问题很多。

接下来考虑一下这种模式的可读性。
使用枚举的大多数场合,我都需要方便得到枚举类型的字符串表达式。如果将int枚举常量打印出来,我们所见到的就是一组数字,这没什么太大的用处。虽然我们可能会想到使用String常量代替int常量。虽然它为这些常量提供了可打印的字符串,但是它会导致性能问题,因为它依赖于字符串的比较操作,所以这种模式也是我们不期望的。

所以 从类型安全性和程序可读性两方面考虑,int和String枚举模式的缺点就显露出来了。

这些问题发生的情况,让Java1.5发行版本开始,就提出了另一种可以替代的解决方案 枚举类型(enum type),可以避免上面的缺点,和提供额外的好处。

枚举定义

枚举类型(enum type)是指由一组固定的常量组成合法的类型。Java中由关键字enum来定义一个枚举类型。下面就是java枚举类型的定义。

public enum Season {
    SPRING, SUMMER, AUTUMN, WINTER;
}

特点

Java定义枚举类型的语句很简约。它有以下特点:
1,使用关键字enum
2,类型名称,比如这里的Season
3,一串允许的值,比如上面定义的春夏秋冬四季
4,枚举可以单独定义在一个文件中,也可以嵌在其它Java类中

其他选项
5,枚举可以实现一个或多个接口(Interface)
6,可以定义新的变量
7,可以定义新的方法
8,可以定义根据具体枚举值而相异的类
9,可以实现 构造函数,对枚举初始化传入一些值

我们用枚举实现之前的例子:

public enum Season {
    SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4);

    private int code;
    //构造函数传入具体数值,让数值与枚举做关联,显现的表示了数值与常量的关联性
    private Season(int code){
        this.code = code;
    }
	
	//可以自定义方法,返回定义的数值
    public int getCode(){
        return code;
    }
}
public class UseSeason {
    /**
     * 将英文的季节转换成中文季节
     * @param season
     * @return
     */
    public String getChineseSeason(Season season){
        StringBuffer result = new StringBuffer();
        switch(season){
            case SPRING :
                result.append("[中文:春天,枚举常量:" + season.name() + ",数据:" + season.getCode() + "]");
                break;
            case AUTUMN :
                result.append("[中文:秋天,枚举常量:" + season.name() + ",数据:" + season.getCode() + "]");
                break;
            case SUMMER : 
                result.append("[中文:夏天,枚举常量:" + season.name() + ",数据:" + season.getCode() + "]");
                break;
            case WINTER :
                result.append("[中文:冬天,枚举常量:" + season.name() + ",数据:" + season.getCode() + "]");
                break;
            default :
                result.append("地球没有的季节 " + season.name());
                break;
        }
        return result.toString();
    }

    public void doSomething(){
    	//直接返回枚举内定义的所有常量的数组对象
        for(Season s : Season.values()){
            System.out.println(getChineseSeason(s));//这是正常的场景
        }
        System.out.println(getChineseSeason(5)); //此处已经是编译不通过了,这就保证了类型安全
       
    }

    public static void main(String[] arg){
        UseSeason useSeason = new UseSeason();
        useSeason.doSomething();
    }
}

枚举用法

1,常量

public enum Color {  
  RED, GREEN, BLANK, YELLOW  
}  

2,switch

enum Signal {  
    GREEN, YELLOW, RED  
}  
public class TrafficLight {  
    Signal color = Signal.RED;  
    public void change() {  
        switch (color) {  
        case RED:  
            color = Signal.GREEN;  
            break;  
        case YELLOW:  
            color = Signal.RED;  
            break;  
        case GREEN:  
            color = Signal.YELLOW;  
            break;  
        }  
    }  
}  

3,向枚举中添加新方法

public enum Color {  
    RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);  
    // 成员变量  
    private String name;  
    private int index;  
    // 构造方法  
    private Color(String name, int index) {  
        this.name = name;  
        this.index = index;  
    }  
    // 普通方法  
    public static String getName(int index) {  
        for (Color c : Color.values()) {  
            if (c.getIndex() == index) {  
                return c.name;  
            }  
        }  
        return null;  
    }  
    // get set 方法  
    public String getName() {  
        return name;  
    }  
    public void setName(String name) {  
        this.name = name;  
    }  
    public int getIndex() {  
        return index;  
    }  
    public void setIndex(int index) {  
        this.index = index;  
    }  
}  

4,覆盖枚举的方法

public enum Color {  
    RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);  
    // 成员变量  
    private String name;  
    private int index;  
    // 构造方法  
    private Color(String name, int index) {  
        this.name = name;  
        this.index = index;  
    }  
    //覆盖方法  
    @Override  
    public String toString() {  
        return this.index+"_"+this.name;  
    }  
}  

5,实现接口

public interface Behaviour {  
    void print();  
    String getInfo();  
}  
public enum Color implements Behaviour{  
    RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);  
    // 成员变量  
    private String name;  
    private int index;  
    // 构造方法  
    private Color(String name, int index) {  
        this.name = name;  
        this.index = index;  
    }  
//接口方法  
    @Override  
    public String getInfo() {  
        return this.name;  
    }  
    //接口方法  
    @Override  
    public void print() {  
        System.out.println(this.index+":"+this.name);  
    }  
}  

6,使用接口组织枚举

public interface Food {  
    enum Coffee {  
        BLACK_COFFEE,DECAF_COFFEE,LATTE,CAPPUCCINO  
    }  
    enum Dessert {  
        FRUIT, CAKE, GELATO  
    }  
}

枚举的实现

我们知道Java里面都是对象,enum就和class一样,只是一个关键字,他并应该也是一个“类”。我们先简单的写一个枚举:

public enum t {
    SPRING,SUMMER;
}

然后我们使用反编译,看看这段代码到底是怎么实现的,反编译后代码内容如下:

public final class T extends Enum
{
    private T(String s, int i)
    {
        super(s, i);
    }
    public static T[] values()
    {
        T at[];
        int i;
        T at1[];
        System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);
        return at1;
    }

    public static T valueOf(String s)
    {
        return (T)Enum.valueOf(demo/T, s);
    }

    public static final T SPRING;
    public static final T SUMMER;
    private static final T ENUM$VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER
        });
    }
}

通过反编译代码我们可以看到,public final class T extends Enum,说明,该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。

当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。

不建议在对外接口中使用枚举

通常在远处调用RPC时候可能会出现这种错误:

java.lang.IllegalArgumentException: 
No enum constant com.a.b.c.AType.P_M

大概就是以上的内容,看起来还是很简单的,提示的错误信息就是在AType这个枚举类中没有找到P_M这个枚举项。

基本原因,在线上开始有这个异常之前,该应用依赖的一个下游系统有发布,而发布过程中是一个API包发生了变化,主要变化内容是在一个RPC接口的Response返回值类中的一个枚举参数AType中增加了P_M这个枚举项。

但是下游系统发布时,并未通知到上游系统依赖的二包进行升级(所以上游的二方包未升级导致缺失 P_M这个枚举项),所以就报错了。

关于这个问题,其实在《阿里巴巴Java开发手册》中也有类似的约定:
在这里插入图片描述
思考:为什么参数中可以有枚举?

异常

Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类。

主要分为三种类型的异常:

1,检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
2,运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
3,错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error和异常Exception

Java异常层次结构图如下图所示:

在这里插入图片描述
从图中可以看出所有异常类型都是内置类Throwable的子类,因而Throwable在异常类的层次结构的顶层。

接下来Throwable分成了两个不同的分支,一个分支是Error,它表示不希望被程序捕获或者是程序无法处理的错误。另一个分支是Exception,它表示用户程序可能捕捉的异常情况或者说是程序可以处理的异常。其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常。

ErrorException的区别:Error通常是灾难性的致命的错误,是程序无法控制和处理的,当出现这些异常时,Java虚拟机(JVM)一般会选择终止线程;Exception通常情况下是可以被程序处理的,并且在程序中应该尽可能的去处理这些异常。

Java异常又可以分为不受检查异常(Unchecked Exception)检查异常(Checked Exception)

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于检查异常,当程序中可能出现这类异常,要么使用try-catch语句进行捕获,要么用throws子句抛出,否则编译无法通过。

不受检查异常:包括RuntimeException及其子类和Error。

常见的运行时异常

NullPointerException(空指针错误)
IllegalArgumentException(参数错误比如方法入参类型错误)
NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)
ArrayIndexOutOfBoundsException(数组越界错误)
ClassCastException(类型转换错误)
ArithmeticException(算术错误)
SecurityException (安全错误比如权限不够)
UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)

对于运行时异常、错误和检查异常,Java技术所要求的异常处理方式有所不同。

1,由于运行时异常及其子类的不可查性,为了更合理、更容易地实现应用程序,Java规定,运行时异常将由Java运行时系统自动抛出,允许应用程序忽略运行时异常。
2,对于方法运行中可能出现的Error,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。因为,大多数Error异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。
3,对于所有的检查异常,Java规定:一个方法必须捕捉,或者声明抛出方法之外。也就是说,当一个方法选择不捕捉检查异常时,它必须声明将抛出异常。

finally

当异常发生时,通常方法的执行会过早的导致方法返回。例如,如果一个方法打开了一个文件并关闭,然后退出,你不希望关闭文件的代码被异常处理机制跳过。可以使用finally关键字来处理。

finally创建的代码块在try/catch块完成之后另一个try/catch出现之前执行。finally块无论有没有异常抛出都会执行。如果抛出异常,即使没有catch子句匹配,finally也会执行。一个方法将从一个try/catch块返回到调用程序的任何时候,经过一个未捕获的异常或者是一个明确的返回语句,finally子句在方法返回之前仍将执行。这在关闭文件句柄和释放任何在方法开始时被分配的其他资源是很有用。

分析下面finally执行顺序的例子;

class TestFinally{
    static void proc1(){
        try{
            System.out.println("inside proc1");
            throw new RuntimeException("demo");
        }finally{
            System.out.println("proc1's finally");
        }
    }
    static void proc2(){
        try{
            System.out.println("inside proc2");
            return ;
        } finally{
            System.out.println("proc2's finally");
        }
    } 
    static void proc3(){
        try{
            System.out.println("inside proc3");
        }finally{
            System.out.println("proc3's finally");
        }
    }
    public static void main(String [] args){
        try{
            proc1();
        }catch(Exception e){
            System.out.println("Exception caught");
        }
        proc2();
        proc3();
    }
}
inside proc1

proc1's finally

Exception caught

inside proc2
 
proc2's finally
 
inside proc3
 
proc3's finally

1,proc1()抛出了异常中断了try,它的finally子句在退出时执行。
2,proc2的try语句通过return语句返回,但在返回之前finally语句执行。
3,在proc3()中try语句正常执行,没有错误,finally语句也被执行。

try-with-resources 代替try-catch-finally

异常使用建议

1,不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
2,抛出的异常信息一定要有意义。建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出NumberFormatException而不是其父类IllegalArgumentException。
3,使用日志打印异常之后就不要再抛出异常了(两者不要同时存在一段代码逻辑中),避免上游继续重复打印日志。
4,在多重catch块后面,可以增加一个catch(Exception)来处理肯会被遗忘的异常
5,对不确定的代码,可以增加try-catch,处理潜在的异常
6,增加finally语句块去释放占用的资源

参考:
深入解析:String#intern
关于Java的静态

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

df007df

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值