Java IO系列3 字节流之DataInputStream与DataOutputStream

编码与字符集

什么把这个专题放在这,因为DataInputStream与DataOutputStream里有个
readUTF()或writeUTF(String str)方法。

由于这方面的内容我看的资料还有限,难免出错,看到下面的内容后自己去找资料证实。不要相信别人的话,要自己求证。以下内容仅供参考

编码和字符集不是一个概念,字符集表示码点与字符之间的映射关系,至于怎么存储,字符集也不关心,具体存储交给编码,但是编码要依赖于字符集。

ASCII码

用一个字节存储字符,但是只用了7位,即27 = 128个字符
ASCII表上的数字0–31分配给了控制字符,用于控制像打印机等一些外围设备。
数字 32–126 分配给了能在键盘上找到的字符,当您查看或打印文档时就会出现。
数字127代表 DELETE 命令。
这里写图片描述

扩展ASCII

扩展的ASCII字符满足了对更多字符的需求。扩展的ASCII包含ASCII中已有的128个字符(数字0–32显示在下图中),又增加了128个字符,总共是256个。即使有了这些更多的字符,许多语言还是包含无法压缩到256个字符中的符号。因此,出现了一些ASCII的变体来囊括地区性字符和符号。例如,许多软件程序把ASCII表(又称作ISO8859-1)用于北美、西欧、澳大利亚和非洲的语言。
这里写图片描述

因为ASCII就一套,ASCII字符集也可以叫ASCII编码

Unicode字符集及其以Unicode字符集的编码

Unicode字符集下有多个编码方案UTF-8、UTF-16、UTF-32.

1.Unicode并不涉及字符是怎么在字节中表示的,它仅仅指定了字符对应的数字
2.Unicode只是一个用来映射字符和数字的标准。它对支持字符的数量没有限制,也不要求字符必须占两个、三个或者其它任意数量的字节。
3.目前Unicode码点的范围是U+0000~U+10FFFF。U+10FFFF是多大呢?大概是111万,按Unicode官方的说法,就这样了,以后也不扩充了,一百多万足够用了,目前也只是定义了10万多个字符左右

Unicode的范围目前是U+0000~U+10FFFF,理论大小为10FFFF+1=11000016。后一个1代表是65536,因为是16进制,所以前一个1是后一个1的16倍,所以总共有1×16+1=17个的65536的大小,粗略估算为17×6万=102万,所以这是一个百万级别的数。
为了更好分类管理如此庞大的码点数,把每65536个码点作为一个平面,总共17个平面。

平面,BMP,SP

码点的全部范围可以均分成17个65536大小的部分,这里面的每一个部分就是一个平面(Plane)。编号从0开始,第一个平面称为Plane 0.

下图来自http://rishida.net/docs/unicode-tutorial/part2

这里写图片描述

第一个平面即是BMP(Basic Multilingual Plane 基本多语言平面),也叫Plane 0,它的码点范围是U+0000~U+FFFF。这也是我们最常用的平面,日常用到的字符绝大多数都落在这个平面内。
UTF-16只需要用两字节编码此平面内的字符。
后续的16个平面称为SP(Supplementary Planes,增补平面)。显然,这些码点已经是超过U+FFFF的了,所以已经超过了16位空间的理论上限,对于这些平面内的字符,UTF-16采用了四字节编码。

其中很多平面还是空的,还没有分配任何字符,只是先规划了这么多。
另:有些还属于私有的,如上图中的最后两个Private Use Planes,在此可自定义字符。

BMP平面
下图来自:http://unifoundry.com/pub/unifont-7.0.03/unifont-7.0.03.bmp
这里写图片描述

正则表达式[\u4E00-\u9FA5]来匹配中文位置,严格来说这只是Unicode最主要的一段中文区域。
有的中文也落在了增补平面内。

代理区

这里写图片描述

BMP缩略图中有一片空白,这就是所谓的代理区(Surrogate Area)
可以看到这段空白从D8~DF。其中D800–DBFF属于高代理区(High Surrogate Area),后面的DC00–DFFF属于低代理区(Low Surrogate Area),各自的大小均为4×256=1024。
还可以看到在它之前是韩文的区域,之后E0开始到F8的则是属于私有的(private),可以在这里定义自己专用的字符。

至此我们对Unicode的码点,平面都有了一定的了解,但我们还没有触及一个重要的方面,那就是码点到最终编码的转换,在Unicode中,这称为UTF。

UTF-32

我们说码点最大的10FFFF也就21位,而UTF-32采用的定长四字节则是32位,所以它表示所有的码点不但毫无压力,反而绰绰有余,所以只要把码点的表示形式以前补0的形式补够32位即可。这种表示的最大缺点是占用空间太大。

再来看稍复杂一点的UTF-8。

UTF-8

UTF-8是变长的编码方案,可以有1,2,3,4四种字节组合。在前面的定长与变长篇章我们提到UTF-8采用了高位保留方式来区别不同变长,如下:
这里写图片描述
码点与字节如何对应?

哪些码点用哪种变长呢?可以先把码点变成二进制,看它有多少有效位(去掉前导0)就可以确定了。

  1. 一字节有效编码位有7位,27=128,码点U+0000~U+007F(0~127)使用一字节。

一字节留给了ASCII,所以UTF-8兼容ASCII。

  1. 二字节有效编码位只有5+6=11位,最多只有211=2048个编码空间,所以数量众多的汉字是无法容身于此的了。码点U+0080~U+07FF(128~2047)使用二字节。

注意:这里码点从128~2047,因为去掉了一字节的码点,所以不会占满2048个编码空间,是有冗余的,但你不能把适用于一字节的码点放到这里来编码。下同。

  1. 三字节模式可看到光是保留位就达到4+2+2=8位,相当一字节,所以只剩下两字节16位有效编码位,它的容量实际也只有65536。码点U+0800~U+FFFF(2048~65535)使用三字节编码。

一些汉字字典收录的汉字达到了惊人的10万级别。基本上,常用的汉字都落在了这三字节的空间里,这就是我们常说的汉字在UTF-8里用三字节表示。当然了,这么说并不严谨,如果这10万的汉字都被收录进来的话,那些偏门的汉字自然只能被挤到四字节空间上去了。

  1. 四字节的可以看到它的有效位是3+6+6+6=21位,前面说到最大的码点10FFFF也是21位,U+FFFF以上的增补平面的字符都在这里来表示。

UTF-16

UTF-16是一种变长的2或4字节编码模式。对于BMP内的字符使用2字节编码,其它的则使用4字节组成所谓的代理对来编码。

代理区是UTF-16为了编码增补平面中的字符而保留的,总共有2048个位置,均分为高代理区(D800–DBFF)和低代理区(DC00–DFFF)两部分,各1024,这两个区组成一个二维的表格,共有1024×1024=210×210=24×216=16×65536,所以它恰好可以表示增补的16个平面中的所有字符。
下面的图片来自wiki
这里写图片描述
下图来自http://rishida.net/docs/unicode-tutorial/part2
这里写图片描述

FilterOutputStream与FilterInputStream

这两个都分别是InputStream和OutputStream的子类。也是DataInputStream与DataOutputStream的父类,而且,FilterInputStream和FilterOutputStream是具体的子类,实现了InputStream和OutputStream这两个抽象类中为给出实现的方法。

但是,FilterInputStream和FilterOutputStream仅仅是“装饰者模式”封装的开始,它们在各个方法中的实现都是最基本的实现,都是基于构造方法中传入参数封装的InputStream和OutputStream的原始对象。

比如,在FilterInputStream类中,封装了这样一个属性

protected volatile InputStream in;

而对应的构造方法是:

protected FilterInputStream(InputStream in) {
    this.in = in;
}

read()方法的实现则为:

public int read() throws IOException {
    return in.read();
}

其它方法的实现,以及FilterOutputStream也都是同理类似的。
我们注意到FilterInputStream和FilterOutputStream并没给出其它额外的功能实现,只是做了一层简单地封装。那么实现额外功能的实际是FilterInputStream和FilterOutputStream的各个子类。

DataInputStream与DataOutputStream

这也是比较重要的一对Filter实现。那么说起功能,实际上就不得不提到他们除了extends FilterInputStream/FilterOutputStream外,还额外实现了DataInput和DataOutput接口。

我们可以先来看下DataInput和DataOutput这两个interface。
这里写图片描述 这里写图片描述

而DataInputStream/DataOutputStream这一对实际上所做的也就是这两个接口所定义的方法。再DataInputStream/DataOutputStream中,这些方法做了拼接和拆分字节的工作。通过这些方法,我们可以方便的读取、写出各种我们实际所面对的类型的数据,而不必具体去在字节层面上做细节操作。

public class DataInputStream extends FilterInputStream implements DataInput
public
class DataInputStream extends FilterInputStream implements DataInput {

    //in:数据源
    public DataInputStream(InputStream in) {
        super(in);
    }

    //字节数组,80字节,当我们readUTF时,先不解码,直接把数据存进bytearr里,若果超过80,重新new一个更大的
    private byte bytearr[] = new byte[80];
    //上面的是原始数据字节,chararr是经过解码后的数据,若果超过80,重新new一个更大的
    private char chararr[] = new char[80];

    //把数据读进b[]数组里,循环读取,一个一个字节的读
    public final int read(byte b[]) throws IOException {
        return in.read(b, 0, b.length);
    }

    //把数据从b[]数组的off位置读进len个长度的字节,循环读取,一个一个字节的读
    public final int read(byte b[], int off, int len) throws IOException {
        return in.read(b, off, len);
    }

   //一次读取最多b.length个字节到b[]数组里
    public final void readFully(byte b[]) throws IOException {
        readFully(b, 0, b.length);
    }

    //一次读取最多len个字节到b[]数组里,偏移值off
    //该方法与read(byte b[], int off, int len)签名很像,但是read是一个一字节(可能会出现循环)的读,readFully是整个数据块读
    public final void readFully(byte b[], int off, int len) throws IOException {
        if (len < 0)
            throw new IndexOutOfBoundsException();
        int n = 0;
        while (n < len) {
            int count = in.read(b, off + n, len - n);
            if (count < 0)
                throw new EOFException();
            n += count;
        }
    }

    //跳过多少字节,返回值是实际跳过多少字节取值范围[0,n]
    public final int skipBytes(int n) throws IOException {
        int total = 0;
        int cur = 0;

        //
        while ((total<n) && ((cur = (int) in.skip(n-total)) > 0)) {
            total += cur;
        }

        return total;
    }

   //读取一个boolean值,占据一个字节
    public final boolean readBoolean() throws IOException {
        int ch = in.read();
        if (ch < 0)
            throw new EOFException();
        return (ch != 0);
    }

    //读取一个字节
    public final byte readByte() throws IOException {
        int ch = in.read();
        if (ch < 0)
            throw new EOFException();
        return (byte)(ch);
    }

    //读取一个无符号的字节
    public final int readUnsignedByte() throws IOException {
        int ch = in.read();
        if (ch < 0)
            throw new EOFException();
        return ch;
    }

    //读取一个short型 2个字节,分2步读取一个字节,然后把这2个字节合并,强转short
    public final short readShort() throws IOException {
        int ch1 = in.read();
        int ch2 = in.read();
        if ((ch1 | ch2) < 0)
            throw new EOFException();
        return (short)((ch1 << 8) + (ch2 << 0));
    }

    //读取一个无符号short型 2个字节,分2步读取一个字节,然后把这2个字节合并
    public final int readUnsignedShort() throws IOException {
        int ch1 = in.read();
        int ch2 = in.read();
        if ((ch1 | ch2) < 0)
            throw new EOFException();
        return (ch1 << 8) + (ch2 << 0);
    }

    //读取一个char ,2个字节,然后把这2个字节合并,强转char
    public final char readChar() throws IOException {
        int ch1 = in.read();
        int ch2 = in.read();
        if ((ch1 | ch2) < 0)
            throw new EOFException();
        return (char)((ch1 << 8) + (ch2 << 0));
    }

    //读取一个int ,4字节,然后把这4个字节合并
    public final int readInt() throws IOException {
        int ch1 = in.read();
        int ch2 = in.read();
        int ch3 = in.read();
        int ch4 = in.read();
        if ((ch1 | ch2 | ch3 | ch4) < 0)
            throw new EOFException();
        return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
    }

    //readLong(),readDouble() 时的缓冲区
    private byte readBuffer[] = new byte[8];

    //一次性读取一个long型值 ,8字节,然后把这8个字节合并
    public final long readLong() throws IOException {
        readFully(readBuffer, 0, 8);
        return (((long)readBuffer[0] << 56) +
                ((long)(readBuffer[1] & 255) << 48) +
                ((long)(readBuffer[2] & 255) << 40) +
                ((long)(readBuffer[3] & 255) << 32) +
                ((long)(readBuffer[4] & 255) << 24) +
                ((readBuffer[5] & 255) << 16) +
                ((readBuffer[6] & 255) <<  8) +
                ((readBuffer[7] & 255) <<  0));
    }

    //读取一个float型的值,4字节
    public final float readFloat() throws IOException {
        return Float.intBitsToFloat(readInt());
    }

  //读取一个double型的值,8字节
    public final double readDouble() throws IOException {
        return Double.longBitsToDouble(readLong());
    }

    private char lineBuffer[];

    @Deprecated
    public final String readLine() throws IOException {
        char buf[] = lineBuffer;

        if (buf == null) {
            buf = lineBuffer = new char[128];
        }

        int room = buf.length;
        int offset = 0;
        int c;

loop:   while (true) {
            switch (c = in.read()) {
              case -1:
              case '\n':
                break loop;

              case '\r':
                int c2 = in.read();
                if ((c2 != '\n') && (c2 != -1)) {
                    if (!(in instanceof PushbackInputStream)) {
                        this.in = new PushbackInputStream(in);
                    }
                    ((PushbackInputStream)in).unread(c2);
                }
                break loop;

              default:
                if (--room < 0) {
                    buf = new char[offset + 128];
                    room = buf.length - offset - 1;
                    System.arraycopy(lineBuffer, 0, buf, 0, offset);
                    lineBuffer = buf;
                }
                buf[offset++] = (char) c;
                break;
            }
        }
        if ((c == -1) && (offset == 0)) {
            return null;
        }
        return String.copyValueOf(buf, 0, offset);
    }

    public final String readUTF() throws IOException {
        return readUTF(this);
    }
    public final static String readUTF(DataInput in) throws IOException {
        //先读取2字节的utf的长度信息 2^16-1 = 65535
        //因为writeUTF的时候长度不许>=64K(即65535个字节)
        int utflen = in.readUnsignedShort();
        byte[] bytearr = null;
        char[] chararr = null;

        // 如果in本身是“数据输入流”,  
        // 则,设置字节数组bytearr = "数据输入流"的成员bytearr  
        //     设置字符数组chararr = "数据输入流"的成员chararr  
        // 否则的话,新建数组bytearr和chararr  
        if (in instanceof DataInputStream) {
            DataInputStream dis = (DataInputStream)in;
            if (dis.bytearr.length < utflen){
                dis.bytearr = new byte[utflen*2];
                dis.chararr = new char[utflen*2];
            }
            chararr = dis.chararr;
            bytearr = dis.bytearr;
        } else {
            bytearr = new byte[utflen];
            chararr = new char[utflen];
        }

        //单字节、双字节、三字节
        int c, char2, char3;
        int count = 0;
        int chararr_count=0;
        // 从“数据输入流”中读取数据并存储到字节数组bytearr中;从bytearr的位置0开始存储,存储长度为utflen。  
        // 注意,这里是存储到字节数组!而且读取的是全部的数据。  
        in.readFully(bytearr, 0, utflen);

        //如果是ascii码就直接存在bytearr里面了,毕竟老外写的源码,都是用ascii码机率比较高,就省去下面for中的判断了
        // 将“字节数组bytearr”中的数据 拷贝到 “字符数组chararr”中  
        // 注意:这里相当于“预处理的输入流中单字节的符号”,因为UTF-8是1-4个字节可变的。
        while (count < utflen) {
            c = (int) bytearr[count] & 0xff;
            // UTF-8的单字节数据的值都不会超过127;所以,超过127,则退出。
            if (c > 127) break;
            count++;
            chararr[chararr_count++]=(char)c;
        }

        // 处理完输入流中单字节的符号之后,接下来我们继续处理。  
        while (count < utflen) {
            // 下面语句执行了2步操作。  
            // (01) 将字节由 “byte类型” 转换成 “int类型”。  
            //      例如, “11001010” 转换成int之后,是 “00000000 00000000 00000000 11001010”  
            // (02) 将 “int类型” 的数据左移4位  
            //      例如, “00000000 00000000 00000000 11001010” 左移4位之后,变成 “00000000 00000000 00000000 00001100”  
            c = (int) bytearr[count] & 0xff;
            switch (c >> 4) {
                case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
                    /* 0xxxxxxx*/
                    // 若 UTF-8 是单字节,即 bytearr[count] 对应是 “0xxxxxxx” 形式;  
                    // 则 bytearr[count] 对应的int类型的c的取值范围是 0-7。  
                    count++;
                    chararr[chararr_count++]=(char)c;
                    break;
                 // 若 UTF-8 是双字节,即 bytearr[count] 对应是 “110xxxxx  10xxxxxx” 形式中的第一个,即“110xxxxx”  
                 // 则 bytearr[count] 对应的int类型的c的取值范围是 12-13。  
                case 12: case 13:
                    /* 110x xxxx   10xx xxxx*/
                    count += 2;
                    if (count > utflen)
                        throw new UTFDataFormatException(
                            "malformed input: partial character at end");
                    char2 = (int) bytearr[count-1];
                    if ((char2 & 0xC0) != 0x80)
                        throw new UTFDataFormatException(
                            "malformed input around byte " + count);
                    chararr[chararr_count++]=(char)(((c & 0x1F) << 6) |
                                                    (char2 & 0x3F));
                    break;
                    // 若 UTF-8 是三字节,即 bytearr[count] 对应是 “1110xxxx  10xxxxxx  10xxxxxx” 形式中的第一个,即“1110xxxx”  
                    // 则 bytearr[count] 对应的int类型的c的取值是14 。  
                case 14:
                    /* 1110 xxxx  10xx xxxx  10xx xxxx */
                    count += 3;
                    if (count > utflen)
                        throw new UTFDataFormatException(
                            "malformed input: partial character at end");
                    char2 = (int) bytearr[count-2];
                    char3 = (int) bytearr[count-1];
                    if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80))
                        throw new UTFDataFormatException(
                            "malformed input around byte " + (count-1));
                    chararr[chararr_count++]=(char)(((c     & 0x0F) << 12) |
                                                    ((char2 & 0x3F) << 6)  |
                                                    ((char3 & 0x3F) << 0));
                    break;
                // 若 UTF-8 是四字节,即 bytearr[count] 对应是 “11110xxx 10xxxxxx  10xxxxxx  10xxxxxx” 形式中的第一个,即“11110xxx”  
                // 则 bytearr[count] 对应的int类型的c的取值是15   
                default:
                    /* 10xx xxxx,  1111 xxxx */
                    throw new UTFDataFormatException(
                        "malformed input around byte " + count);
            }
        }
        // The number of chars produced may be less than utflen
        return new String(chararr, 0, chararr_count);
    }
}

public class DataOutputStream extends FilterOutputStream implements DataInput
public
class DataOutputStream extends FilterOutputStream implements DataOutput {
    /**
     * The number of bytes written to the data output stream so far.
     * If this counter overflows, it will be wrapped to Integer.MAX_VALUE.
     */
    protected int written;

    /**
     * bytearr is initialized on demand by writeUTF
     */
    private byte[] bytearr = null;

    public DataOutputStream(OutputStream out) {
        super(out);
    }

    private void incCount(int value) {
        int temp = written + value;
        if (temp < 0) {
            temp = Integer.MAX_VALUE;
        }
        written = temp;
    }

    public synchronized void write(int b) throws IOException {
        out.write(b);
        incCount(1);
    }

    public synchronized void write(byte b[], int off, int len)
        throws IOException
    {
        out.write(b, off, len);
        incCount(len);
    }

    public void flush() throws IOException {
        out.flush();
    }

    public final void writeBoolean(boolean v) throws IOException {
        out.write(v ? 1 : 0);
        incCount(1);
    }

    public final void writeByte(int v) throws IOException {
        out.write(v);
        incCount(1);
    }

    public final void writeShort(int v) throws IOException {
        out.write((v >>> 8) & 0xFF);
        out.write((v >>> 0) & 0xFF);
        incCount(2);
    }

    public final void writeChar(int v) throws IOException {
        out.write((v >>> 8) & 0xFF);
        out.write((v >>> 0) & 0xFF);
        incCount(2);
    }

    public final void writeInt(int v) throws IOException {
        out.write((v >>> 24) & 0xFF);
        out.write((v >>> 16) & 0xFF);
        out.write((v >>>  8) & 0xFF);
        out.write((v >>>  0) & 0xFF);
        incCount(4);
    }

    private byte writeBuffer[] = new byte[8];

    public final void writeLong(long v) throws IOException {
        writeBuffer[0] = (byte)(v >>> 56);
        writeBuffer[1] = (byte)(v >>> 48);
        writeBuffer[2] = (byte)(v >>> 40);
        writeBuffer[3] = (byte)(v >>> 32);
        writeBuffer[4] = (byte)(v >>> 24);
        writeBuffer[5] = (byte)(v >>> 16);
        writeBuffer[6] = (byte)(v >>>  8);
        writeBuffer[7] = (byte)(v >>>  0);
        out.write(writeBuffer, 0, 8);
        incCount(8);
    }

    public final void writeFloat(float v) throws IOException {
        writeInt(Float.floatToIntBits(v));
    }

    public final void writeDouble(double v) throws IOException {
        writeLong(Double.doubleToLongBits(v));
    }
    public final void writeBytes(String s) throws IOException {
        int len = s.length();
        for (int i = 0 ; i < len ; i++) {
            out.write((byte)s.charAt(i));
        }
        incCount(len);
    }

    public final void writeChars(String s) throws IOException {
        int len = s.length();
        for (int i = 0 ; i < len ; i++) {
            int v = s.charAt(i);
            out.write((v >>> 8) & 0xFF);
            out.write((v >>> 0) & 0xFF);
        }
        incCount(len * 2);
    }

    public final void writeUTF(String str) throws IOException {
        writeUTF(str, this);
    }

    static int writeUTF(String str, DataOutput out) throws IOException {
        int strlen = str.length();
        int utflen = 0;
        int c, count = 0;

        /* use charAt instead of copying String to char array */
        for (int i = 0; i < strlen; i++) {
            c = str.charAt(i);
            if ((c >= 0x0001) && (c <= 0x007F)) {
                utflen++;
            } else if (c > 0x07FF) {
                utflen += 3;
            } else {
                utflen += 2;
            }
        }

        if (utflen > 65535)
            throw new UTFDataFormatException(
                "encoded string too long: " + utflen + " bytes");

        byte[] bytearr = null;
        if (out instanceof DataOutputStream) {
            DataOutputStream dos = (DataOutputStream)out;
            if(dos.bytearr == null || (dos.bytearr.length < (utflen+2)))
                dos.bytearr = new byte[(utflen*2) + 2];
            bytearr = dos.bytearr;
        } else {
            bytearr = new byte[utflen+2];
        }

        bytearr[count++] = (byte) ((utflen >>> 8) & 0xFF);
        bytearr[count++] = (byte) ((utflen >>> 0) & 0xFF);

        int i=0;
        for (i=0; i<strlen; i++) {
           c = str.charAt(i);
           if (!((c >= 0x0001) && (c <= 0x007F))) break;
           bytearr[count++] = (byte) c;
        }

        for (;i < strlen; i++){
            c = str.charAt(i);
            if ((c >= 0x0001) && (c <= 0x007F)) {
                bytearr[count++] = (byte) c;

            } else if (c > 0x07FF) {
                bytearr[count++] = (byte) (0xE0 | ((c >> 12) & 0x0F));
                bytearr[count++] = (byte) (0x80 | ((c >>  6) & 0x3F));
                bytearr[count++] = (byte) (0x80 | ((c >>  0) & 0x3F));
            } else {
                bytearr[count++] = (byte) (0xC0 | ((c >>  6) & 0x1F));
                bytearr[count++] = (byte) (0x80 | ((c >>  0) & 0x3F));
            }
        }
        out.write(bytearr, 0, utflen+2);
        return utflen + 2;
    }

    public final int size() {
        return written;
    }
}

参考:字符集与编码

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值