Java个人总结(基础——框架-面试可用)


一、IO流

  1. InputStream:所有字节输入流统一的父类,抽象类
    int read()
    int read(byte[] data)
    int read(byte[] data,int offset,int len)

  2. OutputStream:所有字节输出流统一的父类,抽象类
    write(int data)
    write(byte[] data)
    write(byte[] data,int offset,int len)

  3. FileInputStream:输入流 字节流 节点流

  4. FileOutputStream:输出流 字节流 节点流

  • 它们都是节点流 构造方法允许传入String路径 或者 File对象
  • 它们都是节点流 但是只能连接文件,不能连接目录,否则直接触发异常FileNotFoundException
  • FileInputStream 最常用的是read(byte[]) 无参read效率太低
  • FileOutputStream 最常用的却是write(byte[],int,int),如果不用三参的write 是会形成结尾的冗余数据的
FileInputStream fis = new FileInputStream("源文件");
FileOutputStream fos = new FileOutputStream("目标文件");
byte[] data = new byte[1024];
int len;
while((len = fis.read(data))!=-1){
    fos.write(data,0,len);
}
fos.close();
fis.close();

学会使用TWR语法 在读写完成之后,关闭流【 Try-With-Resources => 带有资源控制的try catch语法】

try(FileInputStream fis=new FileInputStream("源文件");
    FileOutputStream fos=new FileOutputStream("目标文件")) {
    byte[] data=new byte[1024];
    int len;
    while((len=fis.read(data))!=-1){
        fos.write(data,0,len);
    }
}catch(Exception e){
    e.printStackTrace();
}

使用TWR语法需要实现AutoCloseable接口


二、多线程

多线程的三个实现方法

  1. 集成Thread类

    • 自定义线程类继承Thread类
    • 重写run()方法
    • 创建线程对象,调用start()方法启动线程
    public class TestThread extends Thread{
        @Override
        public void run() {
            // 线程体
        }
    
        public static void main(String[] args) {
            // 创建线程对象
            TestThread testThread = new TestThread();
            // 开启一个线程
            testThread.start();
        }
    }
    
  2. 实现Runnable接口

    • 自定义线程类实现Runnable接口
    • 实现run()方法
    • 创建线程对象,调用start()方法启动线程
    public class TestRunnable implements Runnable{
        @Override
        public void run() {
            // 线程体
        }
    
        public static void main(String[] args) {
            // 创建线程对象
            TestRunnable testRunnable = new TestRunnable();
        /*       
            // 创建代理类对象
            Thread thread = new Thread(testRunnable);
            // 开启一个线程
            thread.start();
        */
            new Thread(testRunnable).start();
        }
    }
    

    由于Runnable接口是函数式接口,因此可以使用java8的新特性lamda表达式

    new Thread(()->{
        // 线程体
    }).start();
    // 或者
    Runnable run = () -> {
        // 线程体
    };
    new Thread(run).start();
    
  3. 实现Callable接口

    • 自定义线程类实现Callable接口,需要返回值类型
    • 重写call方法并且抛出异常
    • 创建线程对象
    • 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
    • 提交执行服务:Future<返回值类型> result = ser.submit(线程对象);
    • 获取结果:返回值类型r = result.get();
    • 关闭服务:ser.shutdownNow();
    public class TestCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            return "200";
        }
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            // 创建线程对象
            TestCallable testRunnable = new TestCallable();
            // System.out.println(Runtime.getRuntime().availableProcessors());
            // 创建执行服务,自定义线程池,作用:降低资源消耗,提高响应速度,方便管理
            ExecutorService ser = new ThreadPoolExecutor(
                    1,	// 核心线程池大小
                    Runtime.getRuntime().availableProcessors(),	// CPU密集型(调优)几核CPU就是几 最大核心线程池大小
                    3,	// 超时了没有人调用就会释放
                    TimeUnit.SECONDS,	// 超时单位
                    new LinkedBlockingQueue<>(3),	// 阻塞队列,有3个候客区
                    Executors.defaultThreadFactory(),	// 线程工厂,创建线程
                    new ThreadPoolExecutor.AbortPolicy()	// 拒绝策略
            );
            // 提交执行服务
            Future<String> result = ser.submit(testRunnable);
            System.out.println(result.get());
            // 立刻关闭服务,正在执行的服务也停止  ser.shutdown()关闭服务,正在执行的服务会继续执行完
            ser.shutdownNow();
        }
    }
    

    其有四种拒绝策略

    new ThreadPoolExecutor.AbortPolicy()	// 中止政策:银行满了,还有人进来的话,不处理,抛出异常
    new ThreadPoolExecutor.CallerRunsPolicy()	// 调用方运行策略:哪里来的回哪去
    new ThreadPoolExecutor.DiscardPolicy()	// 放弃策略:队列满了,丢掉任务,不抛出异常
    new ThreadPoolExecutor.DiscardOldestPolicy()	// 丢弃最旧策略:队列满了,尝试和最早的竞争,不抛出异常
    
  4. synchronized和lock

    • synchronized

      • 同步方法:
        public synchronized void hello() { 
            // 代码
        }
        
      • 同步块:
        synchronized(Obj) {  // Obj可以是任何对象,也可以为自己(this),推荐使用共享资源作为同步监视器
            // 代码
        }     
        

      同步方法锁的内容太多,会浪费资源,同步块只锁某个对象

    • lock

      • Lock lock = new ReentrantLock();
        
      • 加锁:

        lock.lock();
        
      • 解锁:

        finally{ 
            lock.unlock(); 
        }
        
    • 两者异同

      • synchronized是关键字,Lock是一个类
      • synchronized(隐式锁)自动释放锁,Lock(显式锁)需要手动释放锁
      • 使用Lock锁。JVM会花费更少时间来调度线程,其性能更好,并且扩展性更好
      • 两者都为非公平锁
    • 死锁
      产生死锁的四个必要条件

      • 互斥条件:一个资源每次只能被一个进程使用
      • 请求与保持条件:一个进程因请求资源而阻塞,对已获得的资源保持不放
      • 不剥夺条件:进程已获得的资源,在未使用完之前,不会被强行剥夺
      • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

      只要破坏其中任意一个或多个就可以避免死锁发生


三、集合

  1. List集合
    List集合是有序,可重复的
    有序:存储和取出的元素顺序一致
    可重复:存储的元素可以重复

    • ArrayList子类是在使用List接口最常用的一个子类,该类利用数组实现List集合操作
    • Vector和ArrayList类似,都是利用数组去实现,在方法上加synchronized,是线程安全的,因此性能不如ArrayList
    • CopyOnWriteArrayList是一个并发包下的类,是一个线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制数组(快照)上进行的,也就是使用了写时复制策略。其读操作不加锁,因此效率比Vector要高许多
    • LinkedList子类是基于链表的形式实现的List接口标准
    • 链表与数组最大的区别在于:链表实现不需要频繁的进行新数组的空间开辟,但是数组在根据索引获取数据时时间复杂度为O(1),链表的时间复杂度为O(n)。
  2. Set集合
    Set集合是无序,不可重复的

    • HashSet散列存放,按 Hash 算法来存储集合中的元素,是用的HashMap来存取数据,因此具有很好的存取、查找、删除性能。两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。
    • LinkedHashSet是 HashSet 的子类,根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的
    • TreeSet有序存放,是 SortedSet 接口的实现类,可以确保集合元素处于排序状态,用的TreeMap来存取数据
    • CopyOnWriteArraySet是一个线程安全的set,以上三个都是线程不安全的,其底层用的是CopyOnWriteArrayList来存取数据的
  3. Map集合
    Map集合是用于保存具有映射关系的数据:key-value

    • HashMap是允许使用null键和null值,与HashSet一样,不保证映射的顺序,并且key是不可重复的
      在1.7中使用的是数组+链表,在1.8中HashMap采用的是数组+链表+红黑树的数据结构(当链表长度大于8且数组长度大于等于64时链表会转成红黑树,当长度低于6时红黑树又会转成链表)
    • HashTable底层是数组+链表,和HashMap不同的是不允许键或值为null的,并且有synchronized修饰,是线程安全的
    • ConcurrentHashMap底层采用分段的数组+链表实现,将map划分成若干部分来实现它的可扩展性和线程安全。采用了锁分段技术,只有在同一个分段内才存在竞态关系,不同的分段锁之间没有锁竞争。每一个segment的读写操作互相不影响
    • LinkedHashMap是 HashMap 的子类,使用了一对双向链表来记录添加元素的顺序
    • TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态
    • Properties是HashTable的子类,该对象用于处理属性文件
  4. 迭代器

    迭代器主要用于遍历集合

    • Iterator
    List<String> l = new ArrayList()<>;
    l.add("aa");
    l.add("bb");
    l.add("cc");
    for (Iterator iter = l.iterator(); iter.hasNext();) {
        String str = (String)iter.next();
        System.out.println(str);
    }
    
    • foreach
    Set<String> strings = new HashSet<String>();
    strings.add("zxc");
    strings.add("asd");
    for (String temp : strings) {
        System.out.println(temp);
    }
    

四、SSM

  1. Spring
    Spring
public ConfigurableApplicationContext run(String... args) {
    //1.创建时间性能监控器
    StopWatch stopWatch = new StopWatch();
    //启动监听器
    stopWatch.start();
    //2.创建内容为null的IOC容器
    ConfigurableApplicationContext context = null;
    //创建异常报告
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
    //3.配置awt(图标)的相关信息
    this.configureHeadlessProperty();
    //4.创建运行侦听器
    SpringApplicationRunListeners listeners = this.getRunListeners(args);
    //启动侦听器
    listeners.starting();

    Collection exceptionReporters;
    try {
        //5.准备运行时环境
        ApplicationArguments applicationArguments = 
            new DefaultApplicationArguments(args);
        ConfigurableEnvironment environment = 
            this.prepareEnvironment(listeners, applicationArguments);
        //6.配置系统参数
        this.configureIgnoreBeanInfo(environment);
        //7.打印图标
        Banner printedBanner = this.printBanner(environment);
        //8.创建IOC容器
        context = this.createApplicationContext();
        //设置异常报告
        exceptionReporters = this.getSpringFactoriesInstances(
            SpringBootExceptionReporter.class, new Class[]{
                ConfigurableApplicationContext.class}, context);
        //9.初始化IOC容器
        this.prepareContext(context, environment, listeners,
                            applicationArguments, printedBanner);
        //10.刷新IOC容器
        this.refreshContext(context);
        //11.刷新后的处理
        this.afterRefresh(context, applicationArguments);
        //监听器停止
        stopWatch.stop();
        if (this.logStartupInfo) {
            (new StartupInfoLogger(this.mainApplicationClass))
            .logStarted(this.getApplicationLog(), stopWatch);
        }
        //12.发布started事件
        listeners.started(context);
        //13.运行器回调
        this.callRunners(context, applicationArguments);
    } catch (Throwable var10) {
        this.handleRunFailure(context, var10, exceptionReporters, listeners);
        throw new IllegalStateException(var10);
    }
}
  1. SpringMVC
    SpringMVC
  1. 用户通过浏览器发起 HttpRequest 请求到前端控制器 (DispatcherServlet);

  2. DispatcherServlet 将用户请求(url,例如:localhost:8080/SpringMVC/hello)发送给处理器映射器 (HandlerMapping);

  3. 处理器映射器 (HandlerMapping)会根据请求,找到负责处理该请求的处理器(url控制器,例如上面的:hello),并将其封装为处理器执行链 返回 (HandlerExecutionChain) 给 DispatcherServlet;

  4. DispatcherServlet 会根据 处理器执行链 中的处理器,找到能够执行该处理器的处理器适配器(HandlerAdaptor) --注,处理器适配器有多个;

  5. 处理器适配器 (HandlerAdaptor) 会调用对应的具体的 Controller这个类;

  6. Controller 将处理结果及要跳转的视图封装到一个对象 ModelAndView 中并将其返回给处理器适配器 (HandlerAdaptor);

  7. HandlerAdaptor 直接将 ModelAndView 交给 DispatcherServlet ,至此,业务处理完毕;

  8. 业务处理完毕后,我们需要将处理结果展示给用户。于是DisptcherServlet 调用 ViewResolver,将 ModelAndView 中的视图名称封装为视图对象;

  9. ViewResolver 视图解析器将封装好的视图 (View) (根据前缀、后缀类型找到对应的视图)对象返回给 DIspatcherServlet;

  10. DispatcherServlet 调用视图对象,让其自己 (View) 进行渲染(将模型数据填充至视图中),形成响应对象 (HttpResponse);

  11. 前端控制器 (DispatcherServlet) 响应 (HttpResponse) 给浏览器,展示在页面上。

  1. SpringBoot
    SpringBoot自动装配原理见:SpringBoot自动装配原理(源码分析)

  2. Mybatis
    在这里插入图片描述

    其中sqlSessionFactory 一般使用单例模式,SqlSesion包含属性,所以线程不安全,所以SqlSessiony一般作为方法的局部变量。
    在这里插入图片描述

  1. Mybatis 的一级缓存原理 ( sqlsession 级别 )
    第一次发出一个查询 sql,sql 查询结果写入 sqlsession 的一级缓存中,缓存使用的数据结构是一个 map。
    key:MapperID+offset+limit+Sql+所有的入参
    value:用户信息
    同一个 sqlsession 再次发出相同的 sql,就从缓存中取出数据。如果两次中间出现 commit 操作(修改、添加、删除),本 sqlsession 中的一级缓存区域全部清空,下次再去缓存中查询不到所以要从数据库查询,从数据库查询到再写入缓存。
  2. 二级缓存原理 ( mapper 级别 )
    二级缓存的范围是 mapper 级别(mapper同一个命名空间),mapper 以命名空间为单位创建缓存数据结构,结构是 map。mybatis 的二级缓存是通过 CacheExecutor 实现的。CacheExecutor其实是 Executor 的代理对象。所有的查询操作,在 CacheExecutor 中都会先匹配缓存中是否存在,不存在则查询数据库。
    key:MapperID+offset+limit+Sql+所有的入参具体使用需要配置:
  3. Mybatis 全局配置中启用二级缓存配置
  4. 在对应的 Mapper.xml 中配置 cache 节点
  5. 在对应的 select 查询节点中添加 useCache=true

五、MySQL

  1. 数据库引擎类型

    • InnoDB存储引擎

    InnoDB是事务型数据库的首选引擎

    1. InnoDB 支持事务(ACID),且默认一条 sql 语句进行一次事务的开启与提交,可以通过手动开启、提交/回滚事务来提高效率。

    2. 支持行锁定和外键

    3. InnoDB 主键索引是聚簇索引,即叶子节点存储了 Key 和对应的数据,而非主键索引叶子节点存储的是 Key 和对应的主键,若查询没有覆盖索引,需要进行二次查询。

    4. InnoDB 中,对于并发的不同事务,表中数据的总行数可能是不同的,记录行数没有具体意义

    5. InnoDB是默认的MySQL引擎

    • MyISAM存储引擎

    MyISAM基于ISAM存储引擎,并对其进行扩展。

    1. 不支持事物
    2. 不支持外键
    3. MyISAM 的锁粒度只支持表锁
    4. MyISAM 索引是非聚簇索引,叶子节点存储的是数据的地址。
    5. MyISAM 会存储表中数据的总行数
    6. MyISAM 可以读取压缩的数据
    7. MyISAM拥有较高的插入、查询速度
    8. 它是在Web、数据仓储和其他应用环境下最常使用的存储引擎之一。
    • MEMORY存储引擎
    1. MEMORY存储引擎将表中的数据存储到内存中
    2. 未查询和引用其他表数据提供快速访问
    • 选择引擎类型

    如果要提供提交、回滚、崩溃恢复能力的事物安全(ACID兼容)能力,并要求实现并发控制,InnoDB是一个好的选择

    如果数据表主要查询记录,则MyISAM引擎能提供较高的处理效率

    如果只是临时存放数据,数据量不大,并且不需要较高的数据安全性,可以选择将数据保存在内存中的Memory引擎,MySQL中使用该引擎作为临时表,存放查询的中间结果

    如果只有INSERT和SELECT操作,可以选择Archive,Archive支持高并发的插入操作,但是本身不是事务安全的。Archive非常适合存储归档数据,如记录日志信息可以使用Archive

  2. 数据库索引

    • 概念:索引本质上是一种数据结构,用于加快 MySQL 查询数据的效率

    • 主键索引:MySQL 数据库表中必须有一个主键索引,若没有指定,会由 MySQL 自动生成一个隐藏的自增主键索引列。

      ALTER TABLE TableName ADD PRIMARY KEY(column_list); 
      
    • 普通索引:一张表可以创建多个普通索引,一个普通索引可以包含多个字段,允许数据重复,允许 NULL 值插入。

      CREATE INDEX IndexName ON `TableName`(`字段名`(length));
      # 或者
      ALTER TABLE TableName ADD INDEX IndexName(`字段名`(length));
      
    • 唯一索引:索引值不能重复,但允许有空值。通过唯一索引精确查找数据的访问过程 type 是 eq_ref,效率仅次于 const。

      CREATE UNIQUE INDEX IndexName ON `TableName`(`字段名`(length));
      # 或者
      ALTER TABLE TableName ADD UNIQUE (column_list); 
      
    • 复合索引:形如(a,b,c)的索引,遵从最左前缀法则。一个索引包含多个列,在数据库操作期间,复合索引比单值索引所需要的开销更小(对于相同的多个列建索引)。如果一个表中的数据在查询时有多个字段总是同时出现则这些字段就可以作为复合索引,形成索引覆盖可以提高查询的效率!

      最左前缀法则:在创建复合索引(a,b,c)时,B+ 树的结构为,先根据 a 进行排序,当 a 相等时,根据 b 进行排序,当 b 相等时,根据c 进行排序,因此最左的字段是全局有序的,而 b、c 字段仅在左边的列确定时才有序,是局部有序
      因此,当对 a 进行范围查找(>,<,!=,like 等)或是没有使用 a 索引时,此时 a 是不确定的,不能保证 b、c 的有序,因而索引失效;当使用(a,c)组合的查询条件时,只会走 a 索引,因为 c 索引无效

    • 全文索引:full-text 索引,需要 like 的模糊查询的字段可以使用全文索引,效率高于 like。

    • 聚簇索引:并不是一种单独的索引类型,而是一种数据存储方式。将数据存储于索引放到了一块,索引结构的叶子节点保存了行数据。

    • 非聚簇索引:不是聚簇索引的二级索引,也叫作辅助索引,都称为非聚簇索引。将数据与索引分开存储,索引结构的叶子节点指向了数据对应的位置。

    • 建立索引原则

      1. 对于经常要用做查询条件的字段
      2. 索引长度最好不要太长,特别是主键索引
      3. 遵从最左前缀法则,建立复合索引时,最常用的条件放最左边,经常作为精确查询的条件放左边,范围查找放右边
      4. 对于经常要进行增删改的字段,根据维护索引数据结构的开销慎重考虑是否建立索引
      5. 对于区分度低的字段,例如性别,建立索引也提高不了多少查询性能,不需要建索引
  3. B+树

    • B- tree
      B-Tree结构的数据可以让系统高效的找到数据所在的磁盘块。为了描述B-Tree,首先定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为一行记录中除主键外的数据。对于不同的记录,key值互不相同。
      在这里插入图片描述

    • B+ tree
      B+Tree是在B-Tree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是用B+Tree实现其索引结构。
      在这里插入图片描述

      通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对B+Tree进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。

    • 为什么选择B+ tree做为索引

      1. 从磁盘I/O效率方面来看:B+树的非叶子节点不存储数据,所以树的每一层就能够存储更多的索引数量,也就是说,B+树在层高相同的情况下,比B树的存储数据量更多,间接会减少磁盘I/O的次数
      2. 从范围查询效率方面来看:在MySQL中,范围查询是一个比较常用的操作,而B+树的所有存储在叶子节点的数据使用了双向链表来关联,所以B+树在查询的时候只需查两个节点进行遍历就行,而B树需要获取所有节点,因此,B+树在范围查询上效率更高。
      3. 从全表扫描方面来看:因为,B+树的叶子节点存储所有数据,所以B+树的全局扫描能力更强一些,因为它只需要扫描叶子节点。而B树需要遍历整个树。
      4. 从自增ID方面来看:基于B+树的这样一种数据结构,如果采用自增的整型数据作为主键,还能更好的避免增加数据的时候,带来叶子节点分裂导致的大量运算的问题
  4. 事务和锁

    • 事务:ACID原则

      1. Atomic 原子性:事务中所有 sql 语句是一个不可拆分的整体,要么同时失败,要么同时成功
      2. Consistency 一致性:事务前后数据总量不变,例如转账
      3. Isolation 隔离性:由存储引擎的事务隔离级别决定
      4. Durable 持久性:事务对数据的修改会持久化存储到磁盘
      • 根据锁的性质分
      1. 共享锁:Select 语句通过加 lock in share mod 加共享锁,可以并发读,屏蔽写操作
      2. 排他锁:Select 语句通过加 for update 加排他锁,增删改语句默认加排他锁,会屏蔽除当前事务外的其他所有操作
      • 根据锁的粒度分
      1. 表锁:锁住整张表
      2. 行锁:锁住单独数据行
      • 主要解决的问题
      1. 数据更新丢失,当并发事务指令交错时,就可能造成 A 对数据的修改被 B 覆盖
      2. 脏读,事务 A 读取了事务 B 修改后还未提交的数据,并在这个数据上进行事务,然而事务 B 最后执行失败进行了回滚,可能导致数据总量前后不一致等问题
      3. 不可重复读,比如有一个事务 A,选择一条数据进行修改,并且修改的条件是性别为男,A 读取了数据后,判断为男性,此时 B 事务将性别修改为女并提交,在 A 进行修改时,就会发现数据前后不一致,从而修改失败
      4. 幻读,幻读与不可重复读的区别在于,不可重复读主要由于数据前后不一致,幻读主要由于数据前后的存在性不一致,例如有事务 A 要添加一条数据,添加前先根据主键进行判断,判断不存在后,还没有来的及添加,事务 B 此时将这条数据加入了表并提交,导致事务 A 添加失败
  5. 数据库优化

    • 对于表优化

      • 数据类型的优化
      1. 尽量不要用 DOUBLE 存储小数,有内存和精度问题,可以使用 INT 按固定倍率进行存储
      2. 尽量不要用 BLOB 数据存放图片等大型文件
      3. 可以用 ENUM 枚举类型来存储状态值
      • 表结构的优化
      1. 适当添加冗余字段,减少多表查询
      2. 垂直分表:将一张表的字段拆开分配到多张表中,一般将常用字段放在一个表中,将不常用字段放在另一张表中
      3. 垂直分库:将一张表中的字段按照业务分配到不同的表中,适合业务耦合度非常低的场景
      4. 水平分表:在同一个数据库中,把一张表的数据分到多张表进行存储,一般按照主键 ID 进行划分
      5. 水平分库:将一张表的数据分到多个数据库中的多张表进行存储,类似于 Redis 分片
    • 对于索引的优化

      1. 经常需要进行前导 like (形如:%like)模糊匹配的,不建索引
      2. 经常使用负向条件的,如 !=,不建索引
      3. 使用时,遵守最左前缀法则
      4. 字段过长的,可以用 prefix 作前缀索引
      5. 条件查询中 or 字段的条件没有使用索引时,其他索引也会失效
      6. 查询时发生运算,也会索引失效,最典型的字符串匹配,字符串值不加 ’ ',MySQL 会进行类型转换,相当于一次运算
      7. 利用覆盖索引,可以从索引中直接获取查询的字段,减少回表查询
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

_咸蛋.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值