面向对象

一、OOP 理念

    OOP,面向对象编程。
    面向对象四大特性: 抽象、封装、继承、多态。

1、抽象

    忽略一个主题中 与当前目标 无关的 那些方面,以便更充分注意 与目标相关的方面。

2、封装

    设计模式七大原则之一的 迪米特法则 就是对于封装的具体要求,即 A 模块使用 B 模块的某个接口行为,对 B 模块中除此行为之外的其他信息直到得尽可能少。比如 耳机的插孔就是提供声音输出的行为接口,只需关心这个插孔是否有相应的耳机标记是否有圆形,有没有声音即可,至于内部 CPU 如何运算音频信息,以及各个电容如何协作,这些问题根本不需要关注。这使模块只需忠于接口、忠于功能实现即可。
    封装 是一种对象功能内聚的表现形式,使模块之间耦合度变低,更具有维护性;继承使子类能够继承父类,获得父类的部分属性和行为,使模块更有复用性
    Java 之父 Gosling 设计的 Object 类,是任何类的默认父类,是对万事万物的抽象(好家伙,怪不得叫 Object 😏),是在哲学方向上进行的延申思考,高度概括了事物的自然行为和社会行为。我们都知道哲学的三大经典问题:我是谁?我从哪里来?我到哪里去? 在 Object 类中,这些问题都可以得到隐约 的解答:

  • 我是谁? getClass() 说明本质上是谁,而 toString() 是当前的名片。
  • 我从哪里来?Object() 构造方法是生产对象的基本方式,clone() 是繁殖对象的另一种方式。
  • 我到哪里去?finalize() 是在对象销毁时触发的方法。
        这里重点介绍 clone() 方法,它分为 浅拷贝、一般深拷贝 和 彻底深拷贝。
        浅拷贝只复制当前对象的所有基本数据类型,以及相应的引用变量,但没有赋值引用变量指向的实际对象;彻底深拷贝是在成功 clone 一个对象之后,此对象与母对象在任何引用路径上都不存在共享的实例对象,但是引用路径递归越深,则越接近 JVM 底层对象,会发现彻底深拷贝实现难度越大,因为 JVM 底层对象可能是完全共享的。介于浅拷贝和彻底深拷贝之间的都是一般深拷贝。总之,慎用 Object 的 clone() 方法来拷贝对象,因为对象的 clone() 方法默认是浅拷贝的,若想实现深拷贝,则需要覆写 clone() 方法实现引用对象的深度遍历式拷贝。
3、继承

    继承是面向对象编程技术的基石,允许创建具有逻辑等级结构的类体系,形成一个继承树,让软件在业务多变的客观条件下,某些基础模块可以被直接复用、间接复用 或 增强复用,父类的能力通过这种方式赋予子类。
    人人都说 继承是 is-a 关系,那么如何衡量当前的继承关系是否满足 is-a 关系呢? 判断标准即 是否符合 里氏代换原则(LSP)。 LSP 是指 任何父类能够出现的地方,子类都能够出现。
    继承的使用成本很低,一个关键字就可以使用别人的方法,似乎更加轻量简单。想复用别人的代码,跳到脑海的第一个反应就是继承它,所以继承就像抗生素一样被滥用。我们传递的理念是 谨慎使用继承,认清继承滥用的危害性,即 方法污染 和 方法爆炸。方法污染 🎨是指 父类具备的行为,通过继承传递给子类,子类并不具备执行此行为的能力。比如 鸟会飞,鸵鸟继承鸟,发现飞不了,这就是方法污染。方法爆炸 💥 是指继承树不断扩大,方法太多啦。在实际故障中,因为方法爆炸,父类的某些方法签名和子类非常相似,在 IDE 中,输入类名 和 点 后,在自动提示的极为相似的方法签名中,可能会出错。综上,提倡 组合优先原则 来扩展类的能力,即 优先采用组合或聚合的类关系来复用其他类的能力,而不是继承。因为过多地使用继承会破坏代码的可维护性,当父类被修改时,会影响到所有继承自它的子类,从而增加程序的维护难度与成本。比如 策略模式,采用接口与组合的方式比采用继承的方式具有更好的可扩展性。

4、多态

    根据运行时的实际对象类型,同一个方法产生不同的运行结果,使同一个行为具有不同的表现形式。
    (一个对象变量可以指示多种实际类型的现象被称为多态。同一个操作作用在不同对象时,会有不同的语义,从而产生不同的结果。)
(1)覆写
     override 译为 覆写,是子类实现接口,或者继承父类时,保持方法签名完全相同,实现不同的方法体,是垂直方向上行为的不同实现。 最终的实现需要在运行期判断,这就是所谓的 动态绑定
    想成功地覆写父类方法,需要满足以下 4 个条件:

  • 访问权限不能变小。
  • 返回类型能够向上转型成为父类的返回类型。这里的”向上转型“ 必须是严格的继承关系,数据类型基本不存在通过继承向上转型的问题。比如 int 和 Integer 是非兼容返回类型,不会自动装箱。
  • 异常也要能向上转型成为父类的异常。
  • 方法名、参数类型及个数必须严格一致。
        为了使编译器准确地判断是否是覆写行为,所有的覆写方法必须加 @Override 注解。@Override 注解还可以避免因权限控制可见范围导致的覆写失败。
        覆写只能针对非静态、非 final、非构造方法。
    (2)重载
        overload 译为重载,方法名称是相同的,但是参数类型或参数个数是不相同的,是水平方向上行为的不同实现。重载又称为 静态绑定
        多态是指在编译层面无法确定最终调用的方法体,以 覆写 为基础来实现面向对象特性,在运行期由 JVM 进行动态绑定,调用合适的覆写方法体来执行。重载是编译器确定方法调用,属于静态绑定,本质上重载的结果是完全不同的方法。
        JVM 在重载方法中,选择合适的目标方法的顺序如下:
    ① 精确匹配
    ② 如果是基本数据类型,自动转换成更大表示范围的基本类型
    ③ 通过自动拆箱和装箱
    ④ 通过子类向上转型继承路线依次匹配
    ⑤ 通过可变参数匹配
    (null 可以匹配任何类对象。)

二、类

1、类的定义

    类的定义由 访问级别、类型、类名、是否抽象、是否静态、泛型标识、继承 或 实现 关键字、父类 或 接口名称 等组成。 类的访问级别有 public 和 无访问控制符,类型为 class、interface、enum。

2、接口与抽象类

    接口 和 抽象类 是对实体类进行更高层次的抽象,仅定义公共行为和特征。

  • 接口与抽象类的共同点是都不能被实例化,但可以定义引用变量指向实例对象。
  • 不同点:
    在这里插入图片描述
    (问🧚‍♀️:接口中的 default 方法有什么好处?
        答🧚‍♂️:实现类无须覆写,其实例可以直接调用。
        抽象类在被继承时体现的是 is-a 关系,接口在被实现时体现的是 can-do 关系。
        接口是顶级的 “类” ,虽然关键字是 interface,但是编译之后的字节码扩展还是 .class。
        当纠结定义接口还是抽象类时,优先推荐定义为接口, 遵循接口隔离原则,按某个维度划分成多个接口,然后再用抽象类去 implements 某些接口,这样做可方便后续的扩展和重构。
        
3、内部类

     内部类是 定义在另一个类中的类。内部类本身就是类的一个属性,使用内部类的原因:
     内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
     内部类可以对同一个包中的其他类隐藏起来。
     当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较快捷。
     内部类可以用 private 、protected、public 、(default)、abstract、final、static 修饰。
     内部类中声明的所有 static 静态域必须是 final, 原因很简单,我们希望一个静态域 只有一个实例,不过对于每个外部对象,会分别有一个单独的内部类实例,如果这个域不是 final ,它就可能不是唯一的。
    ,与其他属性定义方式一致。当然在内部类中定义接口是不推荐的。内部类可以是静态和非静态的,它可以出现在属性定义、方法体 和 表达式 中,甚至可以在匿名类中出现,具体分为如下四种:

(1)静态内部类
    在 JDK 源码中,定义包内可见静态内部类的方式很常见,这样做的好处是:

  • 作用域不会扩散到包外。
  • 可以通过 “外部类.内部类”的方式直接访问。
  • 内部类可以访问外部类中的所有静态属性和方法。

     像线程池 ThreadPoolExecutor 中的四种拒绝机制CallerRunsPolicy、AbortPolicy、DiscardPolicy、DiscardOldestPolicy就是静态内部类。
    静态内部类可以有静态域 和 方法。
    不能访问外部类的普遍成员变量;只能访问外部类中的静态成员 和 静态方法。
    
❓静态内部类 与 普通内部类 的区别❓
    静态内部类 不依赖于外部类实例对象 而被实例化,也就说可以在不创建外部类对象的情况下创建内部类的对象。另外,静态内部类是不持有指向外部类对象的引用的,这个可以自己尝试反编译 class 文件看一下就知道了。

(2)成员内部类
    成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。

❓为什么成员内部类可以访问外部类的成员呢❓
    编译时,会把 成员内部类 单独编译成一个字节码文件,编译器会自动为成员内部类添加一个指向外部类对象的引用。

(3)局部内部类
    定义在方法或者表达式内部。
例 👀:可以在一个方法中定义局部类

public class OutterClass
{

  public void start()
  {
    //在方法中定义局部类
     class partClass implements ActionListener
       { 
           public void actionPerformed(ActionEvent event)
          {
             System.out.println("At the time,the time is"+new Date());
             }   
        }
   }
}

     局部类不能用 public 或 private 访问说明符进行声明。它的作用域被限定在声明这个局部类的 中。
    局部类有一个优势,即 对外部世界可以完全隐藏起来,即使 OutterClass 类的代码也不能访问它。除了 start() 方法,其他方法不知道 局部类 的存在。而局部类可以访问它们的外部类,还可以访问局部变量。

2.3 匿名内部类
(4)匿名内部类
    如 (new Thread(){ }).start() 。

public class OuterClass {
    //成员内部类
    private class InstanceInnerClass {}
    
    //静态内部类
    static class StaticInnerClass{}

    public static void main(String[] args) {
        //匿名内部类
        class MethodClass{}
        
    }
}
 

通常的语法格式:

  new SuperType(construction parameters)
  {
      inner class 
      method and data
   }

    其实,SuperType 可以是接口,于是内部类就要实现 implements 这个接口,也可以是一个类,于是内部类就要扩展 extends 它。
    由于构造器的名字必须与类名相同,而匿名类没有类名,所以,匿名类不能有构造器。取而代之的是,将构造器参数传递给 父类 的构造器,尤其在内部类实现接口的时候,不能有任何构造参数。

❓为什么局部内部类 和 匿名内部类 只能访问局部 final 变量❓
    局部内部类 和 匿名内部类 都会被编译成 class 文件,像局部内部类,其变量的生命周期随着方法执行完毕就结束了,所以 Java 采用复制的方法:如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。使用的变量只是值和方法中 局部变量 的值相等,与 方法中的局部变量 是完全独立的。
    如果在不同方法中 对 变量有修改,就会造成数据不一致,为了解决这个问题, 编译器就限定必须将 变量 限制为 final 类型,不允许对 变量 进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。


static 关键字
  • 静态域
        如果把域定义成 static,每个类中只有一个这样的域,而每个对象 对于 所有的实例域 却都有自己的一份 拷贝 。比如: 需要给每个雇员赋予唯一的标识码:
 class Employee
  {
   private static int nextID=1;
   private int id;
  }

    现在,每个雇员都有自己的 id 域,但这个类的所有实例将共享一个 nextid 。换言之,如果有 1000 个 Employee 类对象,则有 1000 个 实例域 id,但是只有一个静态域 nextId;即使没有一个 雇员对象,静态域 nextId 也存在,它属于类,不属于任何独立的对象。

  • 静态常量
         比如 Math.PI 、System.out 都是静态常量。

  • 静态方法
        静态方法是一种 不能向对象实施操作的方法,不能访问实例域,但是可以访问自身类的静态域。
        可以使用对象访问静态方法。
        使用静态方法的情景:
    (1)一个方法不需要访问对象状态,其所需参数都是通过显式参数提供。
    (2)一个方法只需要访问类的静态域。
    (3)做工厂方法。


    内部类中还可以定义内部类,形成多层嵌套。

4、访问权限控制

在这里插入图片描述
    变量就像是自己的小孩 🤨,要尽量控制在自己的视线范围内,如果作用域太大,无限制地到处乱跑,就会担心其安危。因此,在定义类时,推荐访问控制级别从严处理:
(1)如果不允许通过外部 new 创建对象,构造方法必须是 private。
(2)工具类不允许有 public 或 default 构造方法。
(3)类非 static 成员变量,并且与子类共享,必须是 protetcted。
(4)类非 static 成员变量,并且仅在本类使用,必须是 private。
(5)类 static 成员变量如果仅在本类使用,必须是 private。
(6)若是 static 成员变量,必须考虑是否为 final。
(7)类成员方法只供类内部调用,必须是 private。
(8)类成员方法只对继承类公开,那么限制为 protected 。
    

5、this 与 super

    对象实例化时,至少有一条从本类出发抵达 Object 的通路,而打通这条路的两个主要士兵就是 this 和 super,逢山开路,遇水搭桥。

  • 如果 this 和 super 指代构造方法,则必须位于方法体的第一行。换句话说,在一个构造方法中, this 和 super 只能出现一个,且只能出现一次。
  • · 由于 this 和 super 都在实例化阶段调用,所以 不能在静态方法和静态代码块中使用 this 和 super 关键字。
6、类关系

    类有关系的情况下,包括如下 6 种类型:

  • 继承 extends (is-a)
    【例】小狗🐶 继承于动物,完全符合里氏代换。
  • 实现 implements(can-do)
    【例】小狗实现了狗叫的接口行为。
  • 组合 :类是成员变量 (contains-a)
        组合是一种完全绑定的关系,所有成员共同完成一件使命,它们的生命周期是一样的。 组合体现的是非常强的整体与部分的关系,同生共死,部分不能在整体之间共享。
    【例】头👩只能是身体🙆‍♀️强组合的一部分,两者完全不可分,具有相同的生命周期。
  • 聚合 :类是成员变量 (has-a)
        聚合是一种可以拆分的整体与部分的关系。
    【例】小狗和狗绳之间是暂时聚合关系,狗绳完全可以复用在另一条小狗上。
  • 依赖:是除组合和聚合以外的单向弱关系。比如,使用另一个类的属性、方法,或 以其作为方法的参数输入,或以其作为方法的返回值输出 (depends-a)。
    【例】人喂养小狗,小狗作为参数传人,是一种依赖关系。
  • 关联:是互相平等的依赖关系 (links-a)。
    【例】人可以用信用卡消费,信用卡可以提取到人的信息。
7、序列化

    内存中的数据对象只有转换为二进制才可以进行数据持久化和网络传输。将数据对象转换为二进制流的过程称为 对象的序列化。反之,将二进制流恢复为数据对象的过程称为反序列化。
    序列化需要保留充分的信息以恢复数据对象,但是为了节约存储空间和网络带宽,序列化的二进制又要尽可能小。序列化的常用场景是 RPC 框架的数据传输。常见的序列化方式有三种:
(1)Java 原生序列化
    Java 类通过实现 Serializable 接口来实现该类对象的序列化,这个接口非常特殊,没有任何方法,只起标记作用。 Java 序列化保留了对象类的元数据 (如 类、成员变量、继承类信息等),以及对象数据,兼容性最好,但不支持跨语言,而且性能一般。
    实现 Serializable 接口的类建议设置 serialVersionUID 字段值,如果不设置,那么每次运行时,编译器 会根据类的内部实现,包括类名、接口名、方法 和 属性等来自动生成 serialVersionUID 。如果类的源码有修改,那么重新编译后 serialVersionUID 的取值可能会发生变化。所以,实现 Serializable 接口的类一定要显式地定义 serialVersionUID 属性值。修改类时需要根据兼容性决定是否修改 serialVersionUID 值。

  • 如果是兼容升级,请不要修改 serialVersionUID 字段,避免反序列化失败。
  • 如果是不兼容升级,需要修改 serialVersionUID 字段,避免反序列化混乱。
        使用 Java 原生序列化需注意,Java 反序列化时不会调用类的无参构造方法,而是调用 native 方法将成员变量赋值为 对应类型的初始值。

(2)Hessian 序列化
    Hessian 序列化支持动态类型、跨语言、基于对象传输的网络协议。
    Hessian 会把复杂对象所有属性存储在一个 Map 中进行序列化,所以在 父类、子类 存在同名成员变量的情况下,Hession 序列化时,先序列化子类,然后序列化父类,因此 反序列化结果会导致子类同名成员变量被父类的值覆盖。

(3)JSON 序列化

transient 关键字【防止序列化】

    构建Maven项目:User 类实现了java.io.Serializable 接口,即表明了该类可以被序列化。该类包含隐私信息:密码 password,这个属性不应该被序列化到外部。
    在我们不想序列化到外部的属性前面加上transient关键字,该属性将不会被序列化。

import org.apache.commons.lang3.SerializationUtils;

import java.io.Serializable;

public class User implements Serializable {
String name;
transient String password;

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }

    public User(String name, String password)
{this.name=name;
this.password=password;}

    public static void main(String[] args) {
        User user=new User("ohh","8");
System.out.println("序列化之前"+user.toString());
//先序列化
       byte[] serialize= SerializationUtils.serialize(user);

       //再反序列化
        User newUser=SerializationUtils.deserialize(serialize);

        System.out.println("反序列化结果"+newUser);
    }
}

在这里插入图片描述
    可以看到,加了 transient 的属性是不会被序列化的。

    这时给 password 加上 static :static transient String password;
    运行结果:
在这里插入图片描述
    可以看到,静态属性没有发生改变。其实,不管有没有加transient关键字,静态属性都不会被序列化。(”序列化“ 保存的是对象的状态,静态变量是针对类的状态,因此序列化并不保存静态变量。这里的不能序列化的意思,是序列化信息中不包含这个静态成员域)
总结:
(1)一旦变量被transient修饰,变量将不再是对象持久化的一部分。
(2)transient关键字只能修饰变量,而不能修饰方法和类。
(3)被transient关键字修饰的变量不再能被序列化。
(4)一个静态变量不管是否被transient修饰,均不能被序列化。

三、方法

  • 方法签名包括方法名称和参数列表, 是 JVM 标识方法的唯一索引,不包括返回值,更加不包括访问权限控制符、异常类型等。

  • 在代码风格中,约定每个逗号后必须要有一个空格,不管是形参,还是实参。

  • 无论是对于基本数据类型,还是引用变量,Java 中的参数传递都是 值复制 的传递过程。

例(1) 👀 :

public static void swap(A a1,A a2)
   { // ①
     A temp=a1;
     // ②
     a1=a2;
     // ③
     a2=temp;
    }

  A a1=new A();
  A a2=new A();
  swap(a1,a2);
  //结果并没有改变存储在变量 a1 和 a2 中的对象引用。

在这里插入图片描述
    


    

在这里插入图片描述

例(2) 👀

public class Test
 {
   public static void test(StringBuffer ss1,int n)
    { ss1.append("World");
     n=8;
    }
       public static void main(String[] args)
       { int i=1;
       StringBuffer s1=new StringBuffer("Hello");
       test(s1,i);
       }
 }

在这里插入图片描述
    


    
在这里插入图片描述
    所以调用 test 方法后,s1 的值为 “HelloWorld”,而 i 的值仍为 1 。

例(3) 👀

   public static void change(StringBuffer ss1,StringBuffer ss2)
    {  
      ss1.append("World");
      ss2=ss1;
    }
     StringBuffer s1=new StringBuffer("Hello");
     StringBuffer s2=new StringBuffer("Hello");
     change(s1,s2);

在这里插入图片描述

    


    

在这里插入图片描述

    
    

  • 尽量不要使用可变参数编程。(因为如果使用不当,严重影响代码的可读性和可维护性。)
  • 要避免使用 Object 作为可变参数。
  • 参数预处理:入参保护;参数检验。
  • 在创建类对象时,会先执行父类和子类的静态代码块,然后再执行父类和子类的构造方法。 (① 静态 > 非静态;② 父类 > 子类
  • 静态方法中不能使用实例成员变量 和 实例方法。
  • 静态代码块在类加载的时候就被调用,并且只执行一次。静态代码块是先于构造方法执行的特殊代码块。静态代码块不能存在于任何方法体内,包括类静态方法和属性变量。
  • 在类定义中,类内方法定义顺序依次是 :公有方法 或 保护方法 > 私有方法 > getter/setter 方法。
  • POJO 专指 只包含 getter、setter、toString 方法的简单类。
  • 不要同时定义 toXxx 和 getXxx()。在类定义中,两者同时存在会在 iBATIS、JSON 序列化等场景下引起冲突。
        

四、泛型

    泛型的本质是类型参数化 ,解决不确定具体对象类型的问题。

public class GenericDefinitionDemo<T> {

    //get 方法
    static <String,T,Joy> String get(String string,Joy joy)
    {return string;}

    public static void main(String[] args) {
        Integer first=222;
        Long second=333L;
        //调用上方定义的 get 方法
        Integer result=get(first,second);
    }
}

    以上代码编译正确且能正常运行,get() 是个泛型方法, first 并非是 java.lang.String 类型,而是泛型标识 <String> ,尖括号里的每个元素都指代一种未知类型,String 出现在尖括号里,它就不是 java.lang.String,而仅仅是一个代号。类名后方定义的泛型 <T> 和 get() 前方定义的 <T> 是两个指代,可以完全不同,互不影响;而类名后的 T 与 尖括号里的 T 相同,也是合法的。
    尖括号的位置非常讲究,必须在类名之后 或 返回值之前。
    泛型在定义处只具备执行 Object 方法的能力。 比如如上代码,在 get() 方法内部执行 string.longValue + joy.intValue 是做不到的,此时泛型只能调用 Object 类中的方法,比如 toString()。
    使用泛型的好处:

  • 类型安全。放置的是什么,取出来的自然是什么,不用担心会抛出 ClasscastException 异常。
  • 提升可读性。
  • 代码重用。泛型合并了同类型的处理代码,使代码重用度变高。
        

五、数据类型

  • 基本数据类型是指不可再分的原子数据类型,内存中直接存储此类型的值,通过内存地址即可直接访问到数据,并且此内存区域只能存放这种类型的值。
  • JVM 并没有针对 boolean 数据类型进行赋值的专用字节码指令, boolean falg=false 就是用 ICONST 0,即 常数 0 进行赋值。
  • 引用分成两种数据类型,引用变量(Reference Variable ,refvar)本身和引用指向的对象(Reference Object,refobj)。
  • 作为一个引用变量,不管它是指向包装类、集合类、字符串类 还是 自定义类,refvar 均占 4B 空间。注意它与真正对象 refobj 之间的区别。无论 refobj 是多么小的对象,最小占用的存储空间是 12 B (用于存储基本信息,称为对象头),但由于存储空间分配单位必须是 8B 的倍数,所以初始分配的空间至少是 16 B。
  • 基本数据类型 Int 占用 4 个字节,而对应的包装类 Integer 实例对象占用 16 个字节。
  • Integer 会缓存 -128~127 之间的值,对于 Integer var=? 在 -128 ~ 127 之间的赋值,Integer 对象由 IntegerCache.cache 产生,会复用已有对象,这个区间的 Integer 值可以直接使用 == 进行判断,但是这个区间之外的所有数据都会在堆上产生,并不会复用已有对象。所以 , 推荐所有包装类对象之间值的比较,全部使用 equals() 方法。
  • 事实上,除了 Float 和 Double 以外,其他包装类数据类型都会缓存。
  • Integer 是唯一可以修改缓存范围的包装类,在 VM options 加入参数 -XX:AutoBoxCacheMax=7777。
  • 在选择使用 包装类 还是 基本数据类型时,推荐使用如下方式:
    (1)所有的 POJO 类属性必须使用包装数据类型。
    (2)RPC 方法的返回值 和 参数 必须使用包装数据类型。
    (3)所有的局部变量推荐使用基本数据类型。
        

六、字符串

  • String 是只读字符串,典型的 immutable 对象,对它的任何改动,其实都是创建一个新对象,再把引用指向该对象。
  • StringBuffer 是线程安全的。
  • 在循环体内,字符串的连接方式应该使用 StringBuffer 的 append 方法进行扩展。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值