【Java高级】枚举、泛型、注解、反射、异常处理、多线程、IO流

一.枚举

Java 枚举是一个特殊的类,一般表示一组常量,比如一年的 4 个季节,一年的 12 个月份,一个星期的 7 天,方向有东南西北等。

Java 枚举类使用 enum 关键字来定义,各个常量使用逗号 , 来分割。

例如定义一个颜色的枚举类。

enum Color 
{ 
    RED, GREEN, BLUE; 
} 

以上枚举类 Color 颜色常量有 RED, GREEN, BLUE,分别表示红色,绿色,蓝色。

enum Color 
{ 
    RED, GREEN, BLUE; 
} 
  
public class Test 
{ 
    // 执行输出结果
    public static void main(String[] args) 
    { 
        Color c1 = Color.RED; 
        System.out.println(c1);   //RED
    } 
}

1.1内部类中使用枚举

public class Test 
{ 
    enum Color 
    { 
        RED, GREEN, BLUE; 
    } 
  
    // 执行输出结果
    public static void main(String[] args) 
    { 
        Color c1 = Color.RED; 
        System.out.println(c1);   //RED
    } 
}

每个枚举都是通过 Class 在内部实现的,且所有的枚举值都是 public static final 的。

以上的枚举类 Color 转化在内部类实现:

class Color
{
     public static final Color RED = new Color();
     public static final Color BLUE = new Color();
     public static final Color GREEN = new Color();
}

1.2迭代枚举元素

可以使用 for 语句来迭代枚举元素:

enum Color 
{ 
    RED, GREEN, BLUE; 
} 
public class MyClass { 
  public static void main(String[] args) { 
    for (Color myVar : Color.values()) {
      System.out.println(myVar);
    }
  } 
}
//RED
//GREEN
//BLUE

1.3在Switch中使用枚举类

枚举类常应用于 switch 语句中:

enum Color 
{ 
    RED, GREEN, BLUE; 
} 
public class MyClass {
  public static void main(String[] args) {
    Color myVar = Color.BLUE;

    switch(myVar) {
      case RED:
        System.out.println("红色");
        break;
      case GREEN:
         System.out.println("绿色");
        break;
      case BLUE:
        System.out.println("蓝色");
        break;
    }
  }
}

//输出结果:
蓝色

1.4values(), ordinal() 和 valueOf() 方法

enum 定义的枚举类默认继承了 java.lang.Enum 类,并实现了 java.lang.Serializable 和 java.lang.Comparable 两个接口。

values(), ordinal() 和 valueOf() 方法位于 java.lang.Enum 类中:

  • values() 返回枚举类中所有的值。
  • ordinal()方法可以找到每个枚举常量的索引,就像数组索引一样。
  • valueOf()方法返回指定字符串值的枚举常量。
enum Color 
{ 
    RED, GREEN, BLUE; 
} 
  
public class Test 
{ 
    public static void main(String[] args) 
    { 
        // 调用 values() 
        Color[] arr = Color.values(); 
  
        // 迭代枚举
        for (Color col : arr) 
        { 
            // 查看索引
            System.out.println(col + " at index " + col.ordinal()); 
        } 
  
        // 使用 valueOf() 返回枚举常量,不存在的会报错 IllegalArgumentException 
        System.out.println(Color.valueOf("RED")); 
        // System.out.println(Color.valueOf("WHITE")); 
    } 
}

//输出结果;
RED at index 0
GREEN at index 1
BLUE at index 2
RED

1.5枚举类成员

枚举跟普通类一样可以用自己的变量、方法和构造函数,构造函数只能使用 private 访问修饰符,所以外部无法调用。

枚举既可以包含具体方法,也可以包含抽象方法。 如果枚举类具有抽象方法,则枚举类的每个实例都必须实现它。

enum Color 
{ 
    RED, GREEN, BLUE; 
  
    // 构造函数
    private Color() 
    { 
        System.out.println("Constructor called for : " + this.toString()); 
    } 
  
    public void colorInfo() 
    { 
        System.out.println("Universal Color"); 
    } 
} 
  
public class Test 
{     
    // 输出
    public static void main(String[] args) 
    { 
        Color c1 = Color.RED;   //这里只调用RED实例,结果输出三个,因为如果枚举类具有抽象方法,则枚举类的每个实例都必须实现它
        System.out.println(c1); 
        c1.colorInfo(); 
    } 
}

//输出结果;
Constructor called for : RED
Constructor called for : GREEN
Constructor called for : BLUE
RED
Universal Color

二.泛型

  • Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

    泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

  • 假定我们有这样一个需求:写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,该如何实现?

    答案是可以使用 Java 泛型

    使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。

2.1.泛型方法

可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。

下面是定义泛型方法的规则:

  • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的 )。
  • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
  • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
  • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。

泛型标记符

java 中泛型标记符:

  • E - Element (在集合中使用,因为集合中存放的是元素)
  • T - Type(Java 类)
  • K - Key(键)
  • V - Value(值)
  • N - Number(数值类型)
  • - 表示不确定的 java 类型

2.2实例

下面的例子演示了如何使用泛型方法打印不同类型的数组元素:

public class GenericMethodTest
{
   // 泛型方法 printArray                         
   public static < E > void printArray( E[] inputArray )
   {
      // 输出数组元素            
         for ( E element : inputArray ){        
            System.out.printf( "%s ", element );
         }
         System.out.println();
    }
 
    public static void main( String args[] )
    {
        // 创建不同类型数组: Integer, Double 和 Character
        Integer[] intArray = { 1, 2, 3, 4, 5 };
        Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
        Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };
 
        System.out.println( "整型数组元素为:" );
        printArray( intArray  ); // 传递一个整型数组
 
        System.out.println( "\n双精度型数组元素为:" );
        printArray( doubleArray ); // 传递一个双精度型数组
 
        System.out.println( "\n字符型数组元素为:" );
        printArray( charArray ); // 传递一个字符型数组
    } 
}

//输出结果:
整型数组元素为:
1 2 3 4 5 

双精度型数组元素为:
1.1 2.2 3.3 4.4 

字符型数组元素为:
H E L L O 

2.3泛型类

  • 泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。

    和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

    实例

    public class Box<T> {
       
      private T t;
     
      public void add(T t) {
        this.t = t;
      }
     
      public T get() {
        return t;
      }
     
      public static void main(String[] args) {
        Box<Integer> integerBox = new Box<Integer>();
        Box<String> stringBox = new Box<String>();
     
        integerBox.add(new Integer(10));
        stringBox.add(new String("菜鸟教程"));
     
        System.out.printf("整型值为 :%d\n\n", integerBox.get());
        System.out.printf("字符串为 :%s\n", stringBox.get());
      }
    }
    
    //输出结果:
    整型值为 :10
    
    字符串为 :菜鸟教程
    
    

2.4类型通配符

1、类型通配符一般是使用 ? 代替具体的类型参数。例如 List<?> 在逻辑上是 List,List 等所有 List<具体类型实参> 的父类。

import java.util.*;
 
public class GenericTest {
     
    public static void main(String[] args) {
        List<String> name = new ArrayList<String>();
        List<Integer> age = new ArrayList<Integer>();
        List<Number> number = new ArrayList<Number>();
        
        name.add("icon");
        age.add(18);
        number.add(314);
 
        getData(name);
        getData(age);
        getData(number);
       
   }
 
   public static void getData(List<?> data) {
      System.out.println("data :" + data.get(0));
   }
}

//输出结果:
data :icon
data :18
data :314

解析: 因为 getData() 方法的参数是 List<?> 类型的,所以 name,age,number 都可以作为这个方法的实参,这就是通配符的作用。

2、类型通配符上限通过形如List来定义,如此定义就是通配符泛型值接受Number及其下层子类类型。

import java.util.*;
 
public class GenericTest {
     
    public static void main(String[] args) {
        List<String> name = new ArrayList<String>();
        List<Integer> age = new ArrayList<Integer>();
        List<Number> number = new ArrayList<Number>();
        
        name.add("icon");
        age.add(18);
        number.add(314);
 
        //getUperNumber(name);//1
        getUperNumber(age);//2
        getUperNumber(number);//3
       
   }
 
   public static void getData(List<?> data) {
      System.out.println("data :" + data.get(0));
   }
   
   public static void getUperNumber(List<? extends Number> data) {
          System.out.println("data :" + data.get(0));
       }
}

//输出结果:
data :18
data :314

解析://1 处会出现错误,因为 getUperNumber() 方法中的参数已经限定了参数泛型上限为 Number,所以泛型为 String 是不在这个范围之内,所以会报错。

3、类型通配符下限通过形如 List<? super Number> 来定义,表示类型只能接受 Number 及其上层父类类型,如 Object 类型的实例。

三.注解与反射

3.1注解

  • Annotation是从JDK5.0开始引入的技术

  • Annotation的作用:

    • 不是程序本身,可以对程序做出解释(这一点和注释(comment)没什么区别)
    • 可以被其他程序(比如:编译器等)读取
  • 注解是以“@注释名”在代码中存在的,还可以添加一些参数值,例如:@SuppressWarning(Values=“zheshizhujie”),

  • Annotation在哪里使用?

    • 可以附加在package,class,method,field等上面,相当于给他们添加了额外的辅助信息,我们可以通过反射机制编程实现对这些元数据的访问

3.1.1什么是注解

public class Test01 extends Object{

    @Override   //重写注解
    public String toString() {
        return super.toString();
    }
}

3.1.2内置注解

  1. @Override:定义在java.lang.Override中,表示一个方法声明打算重写超类中的另一个方法声明
  2. @Deprecated:定义在java.lang.Deprecated中,此注释可以用于修辞方法,属性,类,表示不鼓励程序员使用这样的元素,通常是因为它很危险或者存在更好的选择
  3. @SuppressWarning:定义在java.lang.SuppressWarning中,用来镇压编译时的警告信息
import java.util.ArrayList;
import java.util.List;

public class Test01 extends Object{

    @Override   //重写注解
    public String toString() {
        return super.toString();
    }

    @Deprecated
    public static void test(){
        System.out.println("这是Deprecated");
    }
    @SuppressWarnings("all")
    public void test02(){
        List list = new ArrayList();
    }


    public static void main(String[] args) {
        test();
    }
}

3.1.3元注解

  • 作用:负责注解其他注解,java定义了4个标准的meta—annotation类型,他们被用来提供对其他annotation类型作说明
  • 这些类型和他们所支持的类在java.lang.annotation包中可以找到(@Target,@Retention,@Documented,@Inherited)
    • @Target:用于描述注解的适用范围(即:被描述的注解可以用在什么地方)
    • @Retention:表示需要在什么级别保存该注释信息,用于描述注解的生命周期
      • (SOURCE<CLASS<RUNTIME)
    • @Documented:说明该注释将被包含在javadoc中
    • @Inherited:说明子类可以继承父类中的该注解
import java.lang.annotation.*;

//测试元注解
@Test02.MyAnnotation
public class Test02 {
    public void test(){

    }
//定义一个注解
//Target  表示我们的注解可以用在哪些地方
    @Target(value = {ElementType.METHOD,ElementType.TYPE})

//Retention  表示我们的注解在什么地方还有效
// runtime>class>source
    @Retention(value = RetentionPolicy.RUNTIME)

//Documented 表示是否将我们的注解生成在javadoc中
    @Documented

//Inherited  子类可以继承父类的注解
    @Inherited
    @interface MyAnnotation{

    }
}

3.1.4自定义注解

  • Java中的自定义注解允许开发者定义自己的注解类型,以便在代码中使用。自定义注解可以用于提供元数据,这些元数据可以在编译时或运行时被工具、框架或应用程序读取和处理。

要定义一个自定义注解,你需要使用@interface关键字。下面是一个简单的自定义注解的例子:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 定义注解的保留策略
@Retention(RetentionPolicy.RUNTIME)
// 定义注解可以应用的目标元素类型
@Target(ElementType.METHOD)
public @interface MyCustomAnnotation {
    // 定义注解的属性
    String value() default "";
    int count() default 0;
}

在这个例子中,我们定义了一个名为MyCustomAnnotation的自定义注解。这个注解有两个属性:value和count。value属性默认为空字符串,count属性默认为0。
我们使用了两个元注解来修饰自定义注解:

  • @Retention(RetentionPolicy.RUNTIME):这个元注解指定了自定义注解的保留策略。RetentionPolicy.RUNTIME意味着注解信息将在运行时保留,可以通过反射API访问。
  • @Target(ElementType.METHOD):这个元注解指定了自定义注解可以应用的目标元素类型。ElementType.METHOD意味着这个注解只能用于方法上。

使用自定义注解的例子:

public class MyClass {
    @MyCustomAnnotation(value = "example", count = 1)
    public void myMethod() {
        // 方法体
    }
}

在这个例子中,我们使用MyCustomAnnotation注解来标记MyClass类的myMethod方法。我们为value和count属性提供了值。
在运行时,你可以使用Java的反射API来读取这些注解和它们的属性值。例如:

public class AnnotationExample {
    public static void main(String[] args) {
        try {
            // 获取 MyClass 类的 Class 对象
            Class<?> myClass = Class.forName("MyClass");

            // 获取 myMethod 方法的 Method 对象
            Method method = myClass.getMethod("myMethod");

            // 检查 myMethod 是否使用了 MyCustomAnnotation 注解
            if (method.isAnnotationPresent(MyCustomAnnotation.class)) {
                // 获取 MyCustomAnnotation 注解的实例
                MyCustomAnnotation annotation = method.getAnnotation(MyCustomAnnotation.class);

                // 获取注解的属性值
                String value = annotation.value();
                int count = annotation.count();

                System.out.println("value: " + value);
                System.out.println("count: " + count);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在这个例子中,我们使用反射API来检查myMethod方法是否使用了MyCustomAnnotation注解,并获取注解的属性值。
自定义注解是Java语言中一个非常强大的特性,它允许开发者以声明式的方式添加元数据,这些元数据可以在运行时被查询和利用。

3.2反射

Java 反射(Reflection)是 Java 编程语言的一个特性,它允许运行时的程序获取自身的信息,并且能够操作类和对象的属性、方法等。简单来说,反射就是程序在运行时可以查看和修改自己的行为。
Java 反射机制主要涉及以下类:

  • Class:表示正在运行的 Java 应用程序中的类和接口。
  • Field:提供有关类和接口的字段的信息,以及对它们的动态访问权限。
  • Method:提供有关类和接口的方法的信息,以及对它们的动态访问和调用。
  • Constructor:提供有关类的构造方法的信息,以及对它们的动态访问。

以下是一个简单的 Java 反射的例子:

public class Student {
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void show() {
        System.out.println("Name: " + name + ", Age: " + age);
    }
}

public class ReflectionExample {
    public static void main(String[] args) {
        try {
            // 获取 Student 类的 Class 对象
            Class<?> studentClass = Class.forName("Student");

            // 创建 Student 类的实例
            Object student = studentClass.newInstance();

            // 获取 Student 类的 show 方法
            Method showMethod = studentClass.getMethod("show");

            // 调用 show 方法
            showMethod.invoke(student);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在这个例子中,我们首先使用 Class.forName(“Student”) 获取 Student 类的 Class 对象。然后,我们使用 newInstance() 方法创建一个 Student 对象。接着,我们使用 getMethod(“show”) 获取 Student 类的 show 方法,并使用 invoke(student) 调用这个方法。
这就是 Java 反射的基本用法。通过反射,你可以在运行时动态地创建对象、调用方法、访问字段等。这在很多框架和工具中非常有用,例如 Spring、Hibernate 等。

四.异常处理

4.1异常的概述

  • Java中的异常处理是一种用于处理程序运行时出现的错误和异常情况的结构化机制。异常提供了将错误处理逻辑与常规程序逻辑分离的方法,使得代码更加清晰和易于维护。
  • 在Java中,异常是对象,它们都是Throwable类的实例。Throwable类有两个主要的子类:Error和Exception。

Error
Error类表示编译时和系统错误(外部错误),通常情况下,应用程序无法恢复或不应尝试恢复。例如,OutOfMemoryError是当JVM没有足够的内存进行对象 分配时抛出的错误。

Exception

Exception类是程序中可以处理的异常的父类。它又分为两个子类:RuntimeException和非RuntimeException(也称为检查型异常)。

  • RuntimeException:这些异常是编译器不需要检查的异常,例如NullPointerException、ArrayIndexOutOfBoundsException等。它们通常表示编程错误,应该在编码阶段避免。
  • 非RuntimeException(检查型异常):这些异常是编译器要求必须处理的异常,例如IOException、SQLException等。它们表示程序在运行时可能遇到的预期之外的错误,例如文件不存在或网络连接中断。

非检查型异常(Unchecked Exceptions)

  • NullPointerException:当试图使用null对象引用进行操作时抛出。
  • ArrayIndexOutOfBoundsException:当试图访问数组的非法索引时抛出。
  • ClassCastException:当试图将对象强制转换为不是实例的子类时抛出。
  • IllegalArgumentException:当向方法传递了一个不合法或不适当的参数时抛出。
  • NumberFormatException:当试图将字符串转换为数字,但该字符串没有适当的格式时抛出。
  • ArithmeticException:当出现异常的算术条件,如除以零时抛出。
  • IllegalStateException:当在非法或不适当的时间调用方法时抛出。
  • UnsupportedOperationException:当对象不支持请求的操作时抛出。

检查型异常(Checked Exceptions)

  • IOException:当发生某种I/O问题,如文件不存在或无法读取时抛出。
  • SQLException:当在执行SQL操作时遇到问题时抛出。
  • ClassNotFoundException:尝试加载类时,找不到类时抛出。
  • EOFException:当预期中应该读取更多数据时却到达文件末尾时抛出。
  • FileNotFoundException:尝试访问不存在的文件时抛出。
  • NoSuchMethodException:尝试访问不存在的方法时抛出。
  • InterruptedException:当一个线程在等待或休眠时被另一个线程中断时抛出。

错误(Errors)

  • OutOfMemoryError:当JVM没有足够的内存时抛出。
  • StackOverflowError:当应用递归调用到深层次,导致栈内存用尽时抛出。
  • NoClassDefFoundError:当Java虚拟机找不到类定义时抛出。
  • AssertionError:当断言失败时抛出。

4.2异常的处理

Java提供了以下关键字来处理异常:

  • try:尝试执行可能产生异常的代码块。

  • catch:捕获异常并处理它。

  • finally:无论是否发生异常,都会执行的代码块。

  • throw:在代码中显式抛出一个异常。

  • throws:在方法签名中使用,以声明该方法可能抛出的异常。

    例子:

    public class ExceptionExample {
        public static void main(String[] args) {
            try {
                // 可能抛出异常的代码
                int result = divide(10, 0);
                System.out.println("Result: " + result);
            } catch (ArithmeticException e) {
                // 捕获并处理异常
                System.out.println("Error: " + e.getMessage());
            } finally {
                // 无论是否发生异常,都会执行的代码
                System.out.println("This is the finally block.");
            }
        }
    
        public static int divide(int numerator, int denominator) throws ArithmeticException {
            if (denominator == 0) {
                throw new ArithmeticException("Cannot divide by zero.");
            }
            return numerator / denominator;
        }
    }
    
    

    在这个例子中,divide方法可能会抛出ArithmeticException,因为它包含了除法操作。我们在try块中调用这个方法,并在catch块中处理可能抛出的异常。finally块中的代码无论是否发生异常都会执行。

    可以有多个catch,如果有多个catch的话捕捉catch自上而下。

    在这里插入图片描述

4.3异常的相关关键字介绍

throws关键字:

在Java中,throws关键字用于声明一个方法可能抛出的异常。这允许方法的调用者在调用该方法时知道可能需要处理的异常类型。throws关键字后面跟着一个或多个异常类型,用逗号分隔。
当一个方法声明它throws一个异常时,它意味着该方法不打算处理这个异常,而是将异常传递给它的调用者。调用者 then 需要处理这个异常,要么通过自己的try-catch块,要么通过在它的方法签名中继续声明throws。

举个例子:

public class ExceptionExample {
    public static void main(String[] args) {
        try {
            riskyMethod();
        } catch (IOException e) {
            System.out.println("IOException caught: " + e.getMessage());
        }
    }

    public static void riskyMethod() throws IOException {
        // 假设这个方法可能会抛出一个IOException
        throw new IOException("An I/O error occurred.");
    }
}

在这个例子中,riskyMethod方法声明它可能会抛出IOException。当riskyMethod被调用时,它实际上抛出了一个IOException。这个异常被传递到main方法的try-catch块中,在那里被捕获并处理。
注意,throws关键字只能用于声明检查型异常(Checked Exceptions)。非检查型异常(Unchecked Exceptions,包括RuntimeException和Error)不需要在方法签名中声明,因为它们可以在任何地方被抛出,并且编译器不要求调用者必须处理或声明这些异常。

throw关键字:

​ 在Java中,throw关键字用于显式地抛出一个异常。当你知道一个错误条件已经发生,并且想要通知调用者这个异常情况时,你可以在代码中使用throw语句来抛出一个异常对象。
throw语句后面跟的是一个异常对象,这个对象可以是:
一个已经实例化的异常对象。
一个通过new关键字创建的异常对象。

例子:

public class ThrowExample {
    public static void main(String[] args) {
        try {
            validateAge(15);
        } catch (IllegalArgumentException e) {
            System.out.println("Caught an exception: " + e.getMessage());
        }
    }

    public static void validateAge(int age) {
        if (age < 18) {
            throw new IllegalArgumentException("Age must be at least 18.");
        }
        System.out.println("Age is valid.");
    }
}

在这个例子中,validateAge方法检查传入的年龄是否小于18。如果是,它通过throw语句抛出一个IllegalArgumentException。在main方法中,调用validateAge方法时使用了try-catch块来捕获这个异常。

使用throw关键字时,需要注意以下几点:

  • 异常类型:抛出的异常应该是Throwable的子类,通常是Exception或Error的子类。抛出非Throwable类型的对象会导致编译时错误。
  • 方法签名:如果方法抛出了检查型异常(Checked Exceptions),那么该方法必须声明这些异常,或者在方法内部处理它们。这可以通过在方法签名中使用throws关键字来实现。
  • 资源释放:在抛出异常之前,确保已经释放了任何打开的资源(如文件、网络连接等),以避免资源泄漏。
  • 异常消息:抛出异常时,提供一个清晰的错误消息可以帮助调试和理解异常的原因。

finally关键字:

在Java中,finally关键字用于定义一个代码块,这个代码块在try-catch语句中无论是否发生异常都会被执行。finally块通常用于释放资源或执行一些清理工作,确保无论try块中的代码是否成功执行,这些操作都能得到执行。

finally块的基本语法如下:

try {
    // 尝试执行的代码
} catch (ExceptionType1 e) {
    // 异常处理代码
} catch (ExceptionType2 e) {
    // 异常处理代码
} finally {
    // 无论是否发生异常,都会执行的代码
}

finally块的特点和规则如下:

1.finally块在以下情况下会被执行:

  • try块中的代码正常执行完毕。
  • try块中的代码抛出异常,并且至少有一个catch块能够处理这个异常。
  • try块中的代码抛出异常,但没有catch块能够处理这个异常,异常被传播到上一级。

​ 2.finally块中的代码是在try块或catch块中的代码执行完毕后执行的,即使在这些块中使用了return、continue或break语句也是如此。

  1. 如果finally块中包含return语句,那么这个return语句将覆盖try块或catch块中的return语句。
  2. finally块中抛出的异常会覆盖try块或catch块中抛出的异常。
  3. finally块是可选的,你可以有try块而没有finally块,但如果你有finally块,就必须有try块。

下面是一个使用finally块的例子:

public class FinallyExample {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("file.txt");
            // 读取文件内容
        } catch (FileNotFoundException e) {
            System.out.println("File not found: " + e.getMessage());
        } catch (IOException e) {
            System.out.println("IO error: " + e.getMessage());
        } finally {
            // 关闭文件输入流
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    System.out.println("Error closing file: " + e.getMessage());
                }
            }
        }
    }
}

在这个例子中,无论是否发生异常,finally块都会执行,确保文件输入流fis被正确关闭。这是一个常见的使用场景,因为打开的文件句柄是一种资源,如果不正确关闭,可能会导致资源泄漏。

4.4自定义异常实现

在Java中,自定义异常是通过扩展Exception类或其任何子类来创建的,这允许开发者定义自己的异常类型,以表示应用程序特定的错误条件。自定义异常通常用于增强错误处理的可读性和功能性。

要创建自定义异常,你需要创建一个新类,该类继承自Exception或RuntimeException。选择继承自哪个类取决于你希望自定义异常是检查型还是非检查型。

  • 检查型异常:继承自Exception的异常是检查型异常。这意味着编译器会强制要求处理这些异常,要么通过try-catch块,要么通过在方法签名中使用throws关键字。
  • 非检查型异常:继承自RuntimeException的异常是非检查型异常。编译器不要求处理这些异常,它们可以在代码中的任何地方被抛出,而不需要在方法签名中声明。
    下面是一个简单的自定义异常例子:
// 继承自Exception,因此是检查型异常
public class MyCustomException extends Exception {
    public MyCustomException(String message) {
        super(message);
    }
}

// 继承自RuntimeException,因此是非检查型异常
public class MyRunTimeException extends RuntimeException {
    public MyRunTimeException(String message) {
        super(message);
    }
}

public void doSomething(int value) throws MyCustomException {
    if (value < 0) {
        throw new MyCustomException("Value must be non-negative.");
    }
    // do something with the value
}

自定义异常的好处:

  • 明确性:自定义异常可以提供更具体的错误信息,使得调试和理解错误原因更加容易。
  • 可扩展性:通过创建自定义异常,你可以扩展Java的异常体系,以适应你的应用程序的需求。
  • 封装性:自定义异常可以将特定的业务逻辑和错误处理逻辑封装在一起,使得代码更加模块化。
  • 一致性:在大型应用程序中,使用自定义异常可以帮助保持错误处理的一致性。

五.多线程

1.线程简介

Java 中的多线程(Multithreading)是 Java 并发编程的基础,它允许一个程序同时执行多个任务。这通过创建多个线程来实现,每个线程都是程序执行流的一个实例。在 Java 中,线程是 Thread 类的实例,或者是实现了 Runnable 接口的类的实例。

2.线程实现(重点)

三种创建方式:

Thread class继承Thread类(重点)
Runnable接口实现Runnable接口(重点)
Callable接口实现Callable接口(了解)

Thread:

不推荐使用:避免oop单继承局限性

1.自定义线程类继承Thread类

2.重写run()方法,编写线程执行体

3.创建线程对象,调用start()方法启动线程

//调用run()

package com.Thread_Test;
//创建线程方式一:
public class Thread01 extends Thread{
    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i < 3; i++) {
            System.out.println("重写方法里的线程~~");
        }
    }

    public static void main(String[] args) {
            //main线程,主线程

        //创建一个线程对象
        Thread01 thread01 = new Thread01();

        //调用run()方法    则主线程先执行重写方面里的线程,
        //只有主线程一条执行路径
        thread01.run();
        for (int i = 0; i < 3; i++) {
            System.out.println("main方法里的线程~~");
        }
    }
}


//输出结果:
重写方法里的线程~~
重写方法里的线程~~
重写方法里的线程~~
main方法里的线程~~
main方法里的线程~~
main方法里的线程~~

但如果说复杂度高一点,则是并行交替执行的

//调用start方法
//注意:线程不一定执行,cpu会自己调度安排
package com.Thread_Test;
//创建线程方式一:
public class Thread01 extends Thread{
    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i < 5; i++) {
            System.out.println("重写方法里的线程~~");
        }
    }

    public static void main(String[] args) {
            //main线程,主线程

        //创建一个线程对象
        Thread01 thread01 = new Thread01();

        //调用start()方法    多条执行路径,主线程和子线程并行交替执行
        thread01.start();
        for (int i = 0; i < 500; i++) {
            System.out.println("main方法里的线程~~"+i);
        }
    }
}

//输出结果:   运行结果是交替执行的,线程不一定执行,cpu会自己调度安排
main方法里的线程~~0
重写方法
重写方法
重写方法
重写方法
重写方法
main方法里的线程~~1
main方法里的线程~~2

注意:线程不一定执行,cpu会自己调度安排

Runnable:

推荐使用,避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

1.定义MyRunnable类实现Runnable接口

2.实现run()方法,编写线程执行体

3.创建线程对象,调用start()方法启动线程

```java
package com.Thread_Test;

public class Runnable01 implements Runnable{

    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i < 200; i++) {
            System.out.println("我在看代码..");
        }
    }

    public static void main(String[] args) {
        //创建Runnable接口的实现类对象
        Runnable01 runnable01 = new Runnable01();

        //创建线程对象,通过线程对象来开始我们的线程,代理
       // Thread thread = new Thread(runnable01);
        //thread.start();

        new Thread(runnable01).start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("学习多线程"+i);
        }
    }
}

```

并发问题:

比如买火车票的例子:

package com.Thread_Test;

public class Threar03 implements Runnable{
    private int ticketNums = 5;
    @Override
    public void run() {
        while(true) {
            if (ticketNums<=0){
                break;
            }
            System.out.println(Thread.currentThread().getName() + "--->拿到了第" + ticketNums-- + "票");
        }
    }

    public static void main(String[] args) {
        Threar03 threar03 = new Threar03();
        new Thread(threar03,"小明").start();
        new Thread(threar03,"李一").start();
        new Thread(threar03,"黄牛").start();
    }

}


//输出结果:
小明--->拿到了第5票
黄牛--->拿到了第5票
李一--->拿到了第4票
黄牛--->拿到了第2票
小明--->拿到了第3票
李一--->拿到了第1

输出结果出现了两个人抢到了同一张票的结果

例子:龟兔赛跑

1.龟兔赛跑开始

2.故事中是乌龟赢,兔子需要睡觉,所以我们来模拟兔子睡觉

3.终于,乌龟赢得了比赛

package com.Thread_Test;
//模拟龟兔赛跑
public class match implements Runnable{
    //胜利者
    private static String winner;

    @Override
    public void run() {
        for (int i = 0; i <=100; i++) {
          /*  //模拟兔子休息
            if(Thread.currentThread().getName().equals("兔子")){
                try {
                    Thread.sleep(5);  //让兔子睡觉
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }*/
            //判断比赛是否结束
            boolean flag = gameOver(i);
            //如果比赛结束了,就停止程序
            if(flag){
                break;
            }

            System.out.println(Thread.currentThread().getName()+"-->跑了"+i+"步");
        }
    }

    public boolean gameOver(int steps){
        if(winner!=null){//已经有获胜者
            return true;
        }else{
            if(steps >= 100){
                winner   = Thread.currentThread().getName();
                System.out.println("winner is"+winner);
                return true;
            }
        }

        return false;
    }
//主程序
    public static void main(String[] args) {

        match match = new match();
        new Thread(match,"兔子").start();
        new Thread(match,"乌龟").start();
    }
}

Callable接口:

1.实现Callable接口,需要返回值类型

2.重写call方法,需要抛出异常

3.创建目标对象

4.创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);

5.提交执行:Future result1 = ser.submit(t1);

6.获取结果:boolean r1 = result1.get()

7.关闭服务:ser.shutdownNow();

import java.util.concurrent.*;

// 自定义线程对象,实现Callable接口,重写call()方法
public class MyThread implements Callable<Boolean> {

    @Override
    public Boolean call() throws Exception {
        // 线程执行体
        for (int i = 0; i < 10; i++) {
            System.out.println("我是自定义" + Thread.currentThread().getName() + "--" + i);
        }

        return true;
    }

    public static void main(String[] args) throws ExecutionException,
        InterruptedException {
        // main线程,主线程

        // 创建线程实现类对象
        MyThread thread = new MyThread();
        MyThread thread2 = new MyThread();

        // 创建执行服务,参数是线程池线程数量
        ExecutorService ser = Executors.newFixedThreadPool(2);
        // 提交执行
        Future<Boolean> res = ser.submit(thread);
        Future<Boolean> res2 = ser.submit(thread2);
        // 获取结果
        boolean r1 = res.get();
        boolean r2 = res2.get();
        // 关闭服务
        ser.shutdownNow();
    }
}

静态代理模式

静态代理模式是 Java 中一种设计模式,它允许您为其他对象提供一种代理以控制对这个对象的访问。在静态代理模式中,代理类和目标对象在编译时就已经确定,代理类通常会实现与目标对象相同的接口,并在内部包含一个目标对象的实例,从而可以在不修改目标对象代码的情况下,通过代理对象来扩展目标对象的功能。
静态代理模式通常包含以下三个角色:

  • 1.接口(Subject):定义了代理对象和真实对象需要实现的方法。
  • 2.真实对象(Real Subject):实现了接口,定义了实际业务逻辑。
  • 3.代理对象(Proxy):也实现了接口,并且包含一个真实对象的引用,它在调用真实对象的方法前后可以添加一些额外的操作。

例子:

// 定义一个接口
interface Moveable {
    void move();
}

// 真实对象,实现Moveable接口
class Tank implements Moveable {
    @Override
    public void move() {
        System.out.println("Tank moving...");
    }
}

// 代理对象,也实现Moveable接口
class TankProxy implements Moveable {
    private Tank tank; // 真实对象的引用

    public TankProxy(Tank tank) {
        this.tank = tank;
    }

    @Override
    public void move() {
        // 在调用真实对象的move方法前后,可以添加一些额外的操作
        System.out.println("TankProxy starting...");
        tank.move();
        System.out.println("TankProxy ending...");
    }
}

// 客户端代码
public class StaticProxyExample {
    public static void main(String[] args) {
        Tank tank = new Tank();
        TankProxy tankProxy = new TankProxy(tank);

        tankProxy.move();
    }
}

在这个例子中,Tank 是真实对象,TankProxy 是代理对象。代理对象 TankProxy 包含一个 Tank 类型的成员变量,并在其 move 方法中调用了真实对象的 move 方法。这样,当客户端调用代理对象的 move 方法时,它会先执行一些额外的操作(例如,开始移动前的检查),然后调用真实对象的 move 方法,最后再执行一些结束操作。
静态代理模式的优点是可以不修改目标对象的前提下,通过代理对象对目标对象进行功能扩展。但是,它也有缺点,比如当接口增加方法时,代理对象和真实对象都需要进行修改,这就引入了静态代理模式的局限性。为了解决这个问题,Java 提供了动态代理机制。

Lambda表达式

Lambda 表达式在 Java 中是一种非常方便的语法,它可以让你用一行代码来代替以前需要写很多行代码才能实现的功能。想象一下,如果你要做一个简单的任务,比如打印一些东西到控制台,通常你需要创建一个类来实现一个接口,然后写很多样板代码。Lambda 表达式让你可以跳过这些繁琐的步骤,直接写你想要做的事情。

步骤:

1.定义函数式接口:函数式接口是只有一个抽象方法的接口。你可以使用 @FunctionalInterface 注解来明确声明一个接口是函数式接口。例如:

@FunctionalInterface
interface MyFunctionalInterface {
    void performAction();
}

2.编写 Lambda 表达式:Lambda 表达式通常用于实现函数式接口。它的基本语法是 (parameters) -> { statements; }。如果只有一个参数,并且语句只有一行,可以省略参数类型和大括号。例如:

MyFunctionalInterface myLambda = () -> System.out.println("Hello, Lambda!");

3.使用 Lambda 表达式:一旦你定义了 Lambda 表达式,你就可以像使用其他对象一样使用它。例如,你可以调用函数式接口中的方法:

myLambda.performAction(); // 输出:Hello, Lambda!
package com.Thread_Test;

public class lambda01 {

    @FunctionalInterface            //第一步
    interface MyFunctionalInterface {
        void performAction();
    }

    public static void main(String[] args) {      //第二步
        MyFunctionalInterface myLambda = () -> {
            System.out.println("Hello, Lambda!");
        };
        myLambda.performAction(); // 输出:Hello, Lambda!   //第三部调用
    }
}

4.结合 Lambda 表达式和集合操作:Lambda 表达式经常与 Java 的集合类一起使用,例如 List、Set 和 Map。这些集合类提供了可以接受 Lambda 表达式的方法,如 forEach、filter 和 sort。

import java.util.Arrays;
import java.util.List;

public class LambdaExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

        // 使用 Lambda 表达式遍历列表
        names.forEach(name -> System.out.println(name));
    }
}


//输出结果:
Alice
Bob
Charlie

3.线程状态

方法说明
setPriority(int newPriority)更改线程的优先级
static void sleep(long millis)在指定的毫秒数内让当前正在执行的线程休眠
void join()等待该线程终止
static void yield()暂停当前正在执行的线程对象,并执行其他线程
void interrupt()中断线程,别用这个方式
boolean isAlive()测试线程是否处于活动状态

线程停止

package com.Thread_Test;

public class MyThread implements Runnable{
    /*
     * 测试stop
     * 1.建议线程正常停止   利用次数,不建议死循环
     * 2.建议使用标志位     设置一个标志位
     * 3.不要使用stop或者destroy等过时或者jdk不建议使用的方法
     *
     * */

        //设置一个标志位
    private static volatile boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        while(flag){
            //执行工作。。。
            System.out.println("run..Thread"+i++);
        }
    }

    //设置一个公开的方法停止线程,转换标志位
    public void stop(){
        this.flag = false;
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        new Thread(myThread).start();

        for (int i = 0; i < 1000; i++) {
            System.out.println(i);
            if (i == 900){
                //调用stop()方法切换标志位,让线程停止
                myThread.stop();
                System.out.println("该线程停止了。。。");
            }
        }
    }

}

线程休眠(sleep)

在Java中,线程休眠是通过调用Thread.sleep()方法实现的。当一个线程调用Thread.sleep()方法时,它会告诉操作系统“在接下来的指定毫秒数内,我不需要任何CPU时间”。这期间,线程会暂时让出CPU的使用权,进入“休眠”状态。当指定的时间过去后,线程会重新进入“就绪”状态,等待操作系统分配CPU时间以继续执行。
Thread.sleep()方法有两个重载版本:
1.sleep(long millis):使当前线程暂停执行指定的毫秒数。
2.sleep(long millis, int nanos):使当前线程暂停执行指定的毫秒数加上指定的纳秒数。

需要注意的是,sleep()方法会抛出InterruptedException,这意味着如果一个线程在休眠时被其他线程中断,它将提前醒来并抛出这个异常。因此,调用sleep()方法时,通常需要捕获这个异常。

public class SleepExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                System.out.println("线程开始休眠");
                Thread.sleep(2000);  // 休眠2秒
                System.out.println("线程结束休眠");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        thread.start();
    }
}

在这个示例中,我们创建了一个新的线程,该线程在开始时打印一条消息,然后休眠2秒钟,最后再次打印一条消息。如果线程在休眠期间被中断,它会捕获InterruptedException并打印堆栈跟踪。

使用Thread.sleep()时,重要的是要记住它不会释放任何锁。这意味着如果一个线程持有锁,在休眠期间这个锁仍然被持有,其他线程将无法进入同步块。因此,在设计多线程应用程序时,正确使用sleep()和同步是非常重要的。

线程礼让 (yield)

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让cpu重新调度,礼让不一定成功,看cpu心情

第一个例子:

package com.Thread_Test;

public class YieldExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("线程1: " + i);
                Thread.yield();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("线程2: " + i);
                Thread.yield();
            }
        });

        thread1.start();
        thread2.start();
    }
}


//输出结果:
线程1: 1
线程2: 1
线程2: 2
线程2: 3
线程2: 4
线程1: 2
线程2: 5
线程1: 3
线程1: 4
线程1: 5

第二个例子:

package com.Thread_Test;

public class YieldExample {
    public static void main(String[] args) {
        MyField myField = new MyField();
        new Thread(myField,"a").start();
        new Thread(myField,"b").start();
    }
}

class MyField implements Runnable{
    
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始执行");
        Thread.yield();   //线程礼让
        System.out.println(Thread.currentThread().getName()+"线程开始执行");
    }
}


//输出结果:
b线程开始执行
a线程开始执行
b线程开始执行
a线程开始执行

线程强制执行 (join)

  • join合并线程,待此线程执行完成后,再执行其他线程,其他线程进入阻塞状态
  • 可以简单理解为插队,我称之为霸道线程

用Thread接口举例:

package com.Thread_Test;

public class joinExample {
    public static void main(String[] args) throws InterruptedException {

        //thread01线程
        Thread thread01 = new Thread(() ->{
            for (int i = 1; i <=3; i++) {
                System.out.println("线程1:"+i);
                try {
                    Thread.sleep(1000);  //休眠一下
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);  //抛出一个异常哈
                }
            }
        });

        //thread02线程
        Thread thread02 = new Thread(() ->{
            for (int i = 1; i <=3; i++) {
                System.out.println("线程2:"+i);
                try {
                    Thread.sleep(1000);  //休眠一下
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);  //抛出一个异常哈
                }
            }
        });

        thread01.start();
        // 确保线程1完成执行后再启动线程2
        thread01.join();
        thread02.start();
        // 等待线程2完成执行
        thread02.join();
        System.out.println("所有线程已完成执行");

    }
}

//输出结果:
线程1:1
线程2:1
线程2:2
线程1:2
线程1:3
线程2:3
所有线程已完成执行

在这个示例中,我们创建了两个线程thread01和thread02。我们首先启动thread01,然后在main线程中调用thread01.join(),这意味着main线程将等待thread01完成其执行。一旦thread01完成,main线程继续执行并启动thread02。接着,main线程再次调用thread02.join(),等待thread02完成。最后,当thread02也完成后,main线程打印出"所有线程已完成执行"。
join()方法的一个常见用途是在多线程应用程序中同步线程的执行,确保某些操作按照特定的顺序执行。例如,你可能需要确保在继续执行下一个任务之前,某个数据处理任务已经完成。

用Runnable接口举例:

public class JoinExampleWithRunnable {
    public static void main(String[] args) throws InterruptedException {
        Runnable sharedTask = () -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread thread1 = new Thread(sharedTask, "线程1");
        Thread thread2 = new Thread(sharedTask, "线程2");

        thread1.start();
        // 确保线程1完成执行后再启动线程2
        thread1.join();

        thread2.start();
        // 等待线程2完成执行
        thread2.join();

        System.out.println("所有线程已完成执行");
    }
}

在这个示例中,我们定义了一个Runnable任务sharedTask,它包含了一个打印消息和休眠一秒的循环。然后,我们创建了两个线程thread1和thread2,并将sharedTask作为它们的任务。我们给这两个线程分别命名为"线程1"和"线程2",以便在输出中区分它们。
接下来,我们按照相同的顺序启动和连接线程。首先启动thread1,然后等待它完成执行。一旦thread1完成,我们启动thread2并等待它完成。最后,当两个线程都完成后,main线程打印出"所有线程已完成执行"。
使用Runnable接口的好处是,我们可以轻松地在多个线程之间共享相同的任务代码,这在某些情况下可以简化代码和维护。

线程线程测状态

在Java中,线程可以处于多种状态,这些状态定义在java.lang.Thread.State枚举中。这个枚举包含了线程可能处于的所有状态,以及线程在不同状态之间转换的规则。线程的状态可以帮助开发者理解线程的行为和调试线程相关的问题。

java线程的可能状态包括:

  • 新建状态(NEW):线程已创建,尚未调用start()方法启动之前。
  • 运行状态(RUNNABLE):线程对象被创建后,调用该对象的start()方法,并获取CPU权限进行执行。
  • 阻塞状态(BLOCKED):线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
  • 等待状态(WAITING ):等待状态。正在等待另一个线程执行特定动作来唤醒该线程的状态。
  • 超时等待状态(TIMED_WAITING):线程在指定的时间内等待另一个线程执行特定操作的状态。这种状态通常发生在调用Thread.sleep(long millis)、Object.wait(long timeout)、Thread.join(long millis)等方法时。
  • 终止状态(TERMINATED ):线程已经完成执行的状态。当线程的run()方法执行完毕并自然退出,或者因为一个未捕获的异常而终止时,线程进入这种状态。

要观察线程的状态,可以使用以下方法:

  • getState()方法:Thread类提供了一个getState()方法,它可以返回当前线程的状态。例如,Thread.currentThread().getState()将返回调用线程的状态。
  • ThreadMXBean接口:Java Management Extensions (JMX)提供了一个ThreadMXBean接口,通过这个接口可以获取到关于线程的详细信息,包括线程的状态。ThreadMXBean可以在java.lang.management包中找到。

例子:

public class ThreadStateExample {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        System.out.println("线程状态: " + thread.getState()); // 输出 NEW

        thread.start();
        System.out.println("线程状态: " + thread.getState()); // 输出 RUNNABLE 或 TIMED_WAITING

        Thread.sleep(2000);
        System.out.println("线程状态: " + thread.getState()); // 输出 RUNNABLE 或 TIMED_WAITING

        thread.join();
        System.out.println("线程状态: " + thread.getState()); // 输出 TERMINATED
        
        //      thread.start();   !!!注意,线程结束后就不能再启动了,会报错
    }
}

//输出结果:
线程状态: NEW
线程状态: RUNNABLE
线程状态: TIMED_WAITING
线程状态: TERMINATED

​ 图示:

在这里插入图片描述

4.线程优先级

在Java中,每个线程都有一个优先级,这是一个整数值,用于表示线程的相对重要性。线程优先级通常用于向线程调度器提示哪个线程相对于其他线程应该获得更多的CPU时间。然而,线程优先级并不总是保证线程的执行顺序或CPU时间分配,这取决于底层的操作系统和线程调度策略。
Java中线程的优先级范围是从1到10,其中1是最低优先级,10是最高优先级。默认情况下,新创建的线程继承其父线程的优先级,通常情况下,主线程的优先级是5。

线程优先级可以通过以下方法来设置和获取:

  • setPriority(int newPriority):设置线程的优先级。
  • getPriority():获取线程的优先级。

Thread接口例子:

public class ThreadPriorityExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println("线程1的优先级是: " + Thread.currentThread().getPriority());
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("线程2的优先级是: " + Thread.currentThread().getPriority());
        });

        // 设置线程优先级
        thread1.setPriority(Thread.MIN_PRIORITY); // 设置为最低优先级 1
        thread2.setPriority(Thread.MAX_PRIORITY); // 设置为最高优先级 10

        thread1.start();
        thread2.start();
    }
}


//输出结果:
线程2的优先级是: 10
线程1的优先级是: 1

    
    在这个示例中,我们创建了两个线程thread1和thread2,并分别设置了最低和最高的优先级。然后我们启动这两个线程,并在每个线程的run()方法中打印出其优先级。
需要注意的是,线程优先级的实际效果依赖于底层的操作系统。在某些系统上,线程优先级可能被忽略,而在其他系统上,它们可能被用来影响线程调度。因此,线程优先级不应该被用作程序正确性的关键因素,而应该作为一种优化手段。

Runnable接口例子:

public class ThreadPriorityExampleWithRunnable {
    public static void main(String[] args) {
        Runnable task1 = () -> {
            System.out.println("线程1的优先级是: " + Thread.currentThread().getPriority());
        };

        Runnable task2 = () -> {
            System.out.println("线程2的优先级是: " + Thread.currentThread().getPriority());
        };

        Thread thread1 = new Thread(task1);
        Thread thread2 = new Thread(task2);

        // 设置线程优先级
        thread1.setPriority(Thread.MIN_PRIORITY); // 设置为最低优先级 1
        thread2.setPriority(7); // 设置为7

        thread1.start();
        thread2.start();
    }
}


//输出结果:
线程1的优先级是: 1
线程2的优先级是: 7

    
    在这个示例中,我们定义了两个Runnable任务task1和task2,它们分别打印出执行它们的线程的优先级。然后,我们创建了两个线程thread1和thread2,并将这两个Runnable任务分别赋予它们。接着,我们设置了这两个线程的优先级,并启动了它们。

5.守护(daemon)线程

  • 线程分为用户线程和守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 如.后台记录操作日志,监控内存,垃圾回收等待
public class DaemonThreadExampleWithoutLambda {
    public static void main(String[] args) {
        // 创建守护线程
        Thread daemonThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("守护线程正在运行...");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        // 将线程设置为守护线程
        daemonThread.setDaemon(true);

        daemonThread.start();

        // 主线程继续执行其任务
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程正在运行...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("主线程任务完成,程序即将退出。");
    }
}


//输出结果:
主线程正在运行...
守护线程正在运行...
主线程正在运行...
守护线程正在运行...
主线程正在运行...
守护线程正在运行...
主线程正在运行...
守护线程正在运行...
主线程正在运行...
守护线程正在运行...
守护线程正在运行...
主线程任务完成,程序即将退出。

Runnable接口例子:

package com.Thread_Test;
//上帝守护你
//使用Runnable接口
public class TestDaemon {
    public static void main(String[] args) {
        you you = new you();
        God god = new God();


       Thread t1 = new Thread(god);  //创建线程,默认是false表示是用户线程,
                                          //正常的线程都是用户线程
        t1.setDaemon(true);
        t1.start();   //上帝守护线程开启

        new Thread(you).start();//你再开启线程
    }
}

class God implements Runnable{

    @Override
    public void run() {
        while (true) {
            System.out.println("上帝一直守护着你");
        }
    }
}

class you implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {   //模拟人活1000
            System.out.println("好好活着...");
        }
        System.out.println("我圆寂了....");
    }
}


//输出结果:
...
好好活着...
好好活着...
好好活着...
好好活着...
好好活着...
上帝一直守护着你
上帝一直守护着你
上帝一直守护着你
上帝一直守护着你
上帝一直守护着你
上帝一直守护着你
上帝一直守护着你
上帝一直守护着你
上帝一直守护着你
上帝一直守护着你
上帝一直守护着你
上帝一直守护着你
上帝一直守护着你
上帝一直守护着你
上帝一直守护着你
上帝一直守护着你
上帝一直守护着你
我圆寂了....
上帝一直守护着你
上帝一直守护着你
上帝一直守护着你
上帝一直守护着你
上帝一直守护着你
    ...

6.线程同步(synchronized)

在Java中,线程同步是一种机制,用于控制多个线程对共享资源的访问,以避免并发错误和数据不一致的问题。当多个线程同时访问和修改相同的资源时,如果没有适当的同步措施,就可能会导致数据竞争和不确定的行为。

Java提供了几种线程同步机制:

1.synchronized关键字:Java中的synchronized关键字可以用于方法或代码块,以确保一次只有一个线程可以执行特定的代码段。当一个线程进入一个synchronized方法或代码块时,它会获得与该方法或代码块关联的监视器锁(也称为互斥锁或内部锁),其他线程必须等待该线程释放锁后才能进入相同的synchronized方法或代码块。

  • 同步方法:在方法声明上使用synchronized关键字。
  • 同步代码块:使用synchronized关键字来创建一个同步代码块,指定一个锁对象。

2.volatile关键字:volatile关键字用于声明变量,以确保对变量的读写操作直接在主内存中进行。这可以保证变量的可见性,即一个线程对volatile变量的修改对其他线程立即可见。

3.Lock接口:java.util.concurrent.locks包中的Lock接口提供了比synchronized更灵活的锁定机制。Lock提供了尝试非阻塞地获取锁、尝试获取可中断的锁、尝试获取带有超时的锁等特性。ReentrantLock是实现Lock接口的一个类,它是一个可重入的互斥锁。

4.Semaphore、CountDownLatch、CyclicBarrier等并发工具:这些是java.util.concurrent包中提供的用于同步线程的其他工具类,它们可以帮助线程在特定的条件下等待或同步。

例子:

一.同步方法:

public class SynchronizedExample {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized void decrement() {
        count--;
    }

    public synchronized int getCount() {
        return count;
    }

    public static void main(String[] args) {
        SynchronizedExample example = new SynchronizedExample();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.decrement();
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("最终计数: " + example.getCount());
    }
}


//输出结果:
最终计数: 0

    
    在这个示例中,我们创建了一个SynchronizedExample类,其中包含了一个count变量和三个同步方法:increment()decrement()getCount()。我们创建了两个线程thread1和thread2,分别调用increment()decrement()方法。由于这些方法是同步的,一次只有一个线程可以执行它们,这保证了count变量的更新是线程安全的。最后,我们打印出最终的count值,它应该是0,因为增减操作是相互抵消的。

买票的例子

package com.synchronited.safe;

public class tickets {
    public static void main(String[] args) {
        bugTicket bugTicket = new bugTicket();

        //创建线程
        new Thread(bugTicket,"小张").start();
        new Thread(bugTicket,"小军").start();
        new Thread(bugTicket,"小红").start();
    }
}


class bugTicket implements Runnable{
    private int tickets = 10;  //定义10张票
    private Boolean flag = true; // 外部停止方式
    @Override
    public void run() {
        while(flag){
            try {
                buy();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }


    private synchronized void buy() throws InterruptedException {
        if(tickets<=0){
            flag = false;
            return;
        }            //如果不加synchronized则可能会出现并发问题:
 /*
 private void buy() throws InterruptedException{}
 小张抢到了第10张票
小红抢到了第8张票
小军抢到了第9张票
小军抢到了第7张票
小张抢到了第6张票
小红抢到了第6张票
小张抢到了第5张票
小红抢到了第5张票
小军抢到了第5张票
小张抢到了第4张票
小军抢到了第3张票
小红抢到了第3张票
小红抢到了第2张票
小张抢到了第2张票
小军抢到了第2张票
小红抢到了第1张票
小张抢到了第-1张票
小军抢到了第0张票
*/

        Thread.sleep(1000);//模拟延时

        System.out.println(Thread.currentThread().getName()+"抢到了第"+tickets--+"张票");
    }
}


//输出结果:
小张抢到了第10张票
小张抢到了第9张票
小张抢到了第8张票
小张抢到了第7张票
小张抢到了第6张票
小张抢到了第5张票
小张抢到了第4张票
小张抢到了第3张票
小张抢到了第2张票
小张抢到了第1张票

二.同步代码快

同步代码块可以同步一个方法或一个代码块。下面是同步代码块的基本语法:

synchronized(obj) {
    // 共享资源的操作代码
}
  
//这里的obj被称为同步监视器(或锁),当线程进入同步代码块之前,必须获得obj的锁。如果其他线程已经持有该锁,那么新线程必须等待,直到锁被释放。

例子:

public class Counter {
    private int count = 0;

    public void increment() {
        // 同步代码块
        synchronized (this) {
            count++; // 关键代码:增加计数器的值
            System.out.println(Thread.currentThread().getName() + " count: " + count);
        }
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();

        // 创建两个线程,它们都会调用increment方法
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                counter.increment();
            }
        }, "Thread-1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                counter.increment();
            }
        }, "Thread-2");

        // 启动线程
        t1.start();
        t2.start();

        // 等待两个线程都执行完毕
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出最终的计数器值
        System.out.println("Final count: " + counter.getCount());
    }
}


//输出结果:
Thread-1 count: 1
Thread-1 count: 2
Thread-1 count: 3
Thread-1 count: 4
Thread-1 count: 5
Thread-2 count: 6
Thread-2 count: 7
Thread-2 count: 8
Thread-2 count: 9
Thread-2 count: 10
Final count: 10

    
    在这个例子中,我们创建了一个Counter对象,并定义了一个increment方法。这个方法内部有一个同步代码块,它使用this作为同步监视器。这意味着同一时刻只有一个线程能够执行这个同步代码块中的代码。
在main方法中,我们创建了两个线程t1和t2,它们都会调用increment方法。由于increment方法是同步的,所以即使t1和t2同时运行,它们也不会同时执行count++这一行代码。这意味着计数器的增加是线程安全的。
最后,我们等待两个线程都执行完毕,并输出最终的计数器值。由于increment方法是同步的,所以最终的计数器值应该是两个线程调用increment方法的总次数,即10次。

三.死锁

Java中的死锁是指两个或多个线程永久性地阻塞,等待对方释放锁的情况。这种情况下,涉及的线程都无法继续执行,因为每个线程都在等待另一个线程释放它持有的锁。死锁通常发生在多个线程需要同时获取多个资源,并且每个线程已经持有一个锁而等待获取另一个锁的情况下。

死锁的四个必要条件

1.互斥条件:资源不能被多个线程共享,只能由一个线程在任意时刻使用。

2. 持有和等待条件:线程已经持有至少一个资源,但又提出了新的资源请求,而该资源已被其他线程持有,所以当前线程会等待。
3. .非抢占条件:线程所获得的资源在未使用完之前,不能被其他线程强行抢占。
 
4. 循环等待条件:存在一种线程资源的循环等待链,每个线程至少持有一个资源,并等待获取下一个线程所持有的资源。

死锁示例:

public class DeadlockDemo {

    public static void main(String[] args) {
        final Object resource1 = "Resource1";
        final Object resource2 = "Resource2";

        // Thread-1
        Thread t1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread-1: Locked resource-1");

                try { Thread.sleep(100); } catch (Exception e) {}

                synchronized (resource2) {
                    System.out.println("Thread-1: Locked resource-2");
                }
            }
        });

        // Thread-2
        Thread t2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("Thread-2: Locked resource-2");

                try { Thread.sleep(100); } catch (Exception e) {}

                synchronized (resource1) {
                    System.out.println("Thread-2: Locked resource-1");
                }
            }
        });

        t1.start();
        t2.start();
    }
}


在这个例子中,Thread-1首先锁定resource1,然后尝试锁定resource2。同时,Thread-2首先锁定resource2,然后尝试锁定resource1。由于两个线程都在等待对方释放锁,因此它们将永远等待,导致死锁。

四.Lock锁

Lock 锁也称同步锁,java.util.concurrent.locks.Lock 机制提供了⽐ synchronized 代码块和 synchronized ⽅法更⼴泛的锁定操作,同步代码块 / 同步⽅法具有的功能 Lock 都有,除此之外更强⼤,更体现⾯向对象。
创建对象 Lock lock = new ReentrantLock() ,加锁与释放锁⽅法如下:

  • public void lock() :加同步锁
  • public void unlock() :释放同步锁

synchronized和Lock的对比:

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭),synchronized是隐式锁,除了作用域就自动释放。
  • Lock只是代码块锁(执行体放在开启锁和关闭锁中间),synchronized有代码块锁和方法锁。
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。
  • 优先使用顺序:Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// 测试 Lock锁
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        TestLock testLock = new TestLock();
        new Thread(testLock,"a").start();
        new Thread(testLock,"b").start();
        new Thread(testLock,"c").start();
    }
}

class TestLock implements Runnable {

    // 车票
    private static int tickerNum = 10;

    // 创建一个Lock锁
    private final Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            lock.lock(); // 加锁
            try {
                // 判断是否还有票
                if (tickerNum > 0) {
                    // 模拟延时
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 买票
                    System.out.println(Thread.currentThread().getName() + "线程买到第" + tickerNum -- + "张票");
                } else {
                    break;
                }
            } finally {
                lock.unlock(); // 解锁
            }
        }
    }
}


//输出结果:
a线程买到第10张票
a线程买到第9张票
a线程买到第8张票
a线程买到第7张票
a线程买到第6张票
a线程买到第5张票
b线程买到第4张票
b线程买到第3张票
b线程买到第2张票
b线程买到第1张票

五.线程协作

java中的线程协作是指多个线程在执行过程中,根据某些条件或信号协同工作,以完成特定的任务。线程协作通常涉及到线程之间的通信和同步。Java提供了几个关键字和方法来支持线程协作,包括wait(), notify(), notifyAll(), 以及java.util.concurrent包中的Condition对象。

线程协作的关键机制:

方法名作用
wait()当一个线程调用一个共享对象的wait()方法时,它会进入等待状态,直到另一个线程调用同一个对象的notify()或notifyAll()方法。
notify()唤醒在此对象监视器上等待的单个线程。
notifyAll()唤醒在此对象监视器上等待的所有线程。

注意:均是Object的方法,均只能在同步方法或者同步代码块中使用,否则会抛出异常IIIegalMonitorStageException。

解决线程之间通信的方式:管程法和信号灯法

管程法:

生产者把生产好的数据放入缓存区,消费者从缓存区中拿出数据。

在这里插入图片描述

// 线程通信:生产消费模式-管程法
public class MyThread{
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Productor(container).start();
        new Consumer(container).start();
    }
}

// 产品
class Chicken {
    int id;

    public Chicken (int id) {
        this.id = id;
    }
}

// 生产者
class Productor extends Thread {
    SynContainer synContainer;

    public Productor(SynContainer synContainer) {
        this.synContainer = synContainer;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            synContainer.pushTo(new Chicken(i));
        }
    }
}

// 消费者
class Consumer extends Thread {
    SynContainer synContainer;

    public Consumer(SynContainer synContainer) {
        this.synContainer = synContainer;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            synContainer.popTo();
        }
    }
}

// 容器
class SynContainer {
    // 定义一个大小为10的容器
    Chicken[] chickens = new Chicken[10];
    // 容器计数器
    int count;

    // 生产者生产产品方法
    public synchronized void pushTo(Chicken chicken) {
        // 如果容器满了,就停止生产
        if (chickens.length == count) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 如果容器没满,就往容器中放入产品
        chickens[count] = chicken;
        System.out.println("生产了" + chicken.id + "个鸡腿");
        count ++;

        // 通知消费者消费
        this.notifyAll();
    }

    // 消费者消费产品方法
    public synchronized Chicken popTo() {
        // 如果容器中没有产品了,就停止消费
        if (count == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 如果容器有产品,就可以消费
        count --;
        Chicken chicken = chickens[count];
        System.out.println("消费了第" + chicken.id + "个鸡退");

        //只要消费了,就通知生产者生产
        this.notifyAll();
        return chicken;
    }
}


//输出结果:
生产了0个鸡腿
生产了1个鸡腿
生产了2个鸡腿
生产了3个鸡腿
生产了4个鸡腿
生产了5个鸡腿
生产了6个鸡腿
生产了7个鸡腿
生产了8个鸡腿
生产了9个鸡腿
消费了第9个鸡退
消费了第8个鸡退
消费了第7个鸡退
消费了第6个鸡退
消费了第5个鸡退
消费了第4个鸡退
消费了第3个鸡退
消费了第2个鸡退
消费了第1个鸡退
消费了第0个鸡退
生产了10个鸡腿
生产了11个鸡腿
生产了12个鸡腿
生产了13个鸡腿
生产了14个鸡腿
生产了15个鸡腿
生产了16个鸡腿
生产了17个鸡腿
生产了18个鸡腿
生产了19个鸡腿
消费了第19个鸡退
消费了第18个鸡退
消费了第17个鸡退
消费了第16个鸡退
消费了第15个鸡退
消费了第14个鸡退
消费了第13个鸡退
消费了第12个鸡退
消费了第11个鸡退
消费了第10个鸡退
信号灯法:

设置一个标识位,标识位来判断线程是等待还是执行。

// 线程通信:生产消费模式-信号灯法
public class MyThread{
    public static void main(String[] args) {
        CCTV cctv = new CCTV();
        new Player(cctv).start();
        new Watcher(cctv).start();
    }
}

// 演员
class Player extends Thread {
    CCTV cctv;

    public Player(CCTV cctv) {
        this.cctv = cctv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (i%2 == 0) {
                cctv.play("快乐大本营");
            } else {
                cctv.play("天天向上");
            }
        }
    }
}

// 观众
class Watcher extends Thread {
    CCTV cctv;

    public Watcher(CCTV cctv) {
        this.cctv = cctv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            cctv.watch();
        }
    }
}

// 电视
class CCTV {
    // 表演的节目
    String voice;
    // 标识
    boolean flag = true;

    // 表演节目
    public synchronized void play(String voice) {
        if (!flag) {
            try {
                this.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        this.voice = voice;
        System.out.println("演员表演了:" + voice);
        // 通知观众观看
        this.notifyAll();
        this.flag = !flag;
    }

    // 观看节目
    public synchronized void watch () {
        if (flag) {
            try {
                this.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // 如果容器有产品,就可以消费
        System.out.println("观众观看了:" + voice);
        // 通知演员表演节目
        this.notifyAll();
        this.flag = !flag;
    }
}


//输出结果:
演员表演了:快乐大本营
观众观看了:快乐大本营
演员表演了:天天向上
观众观看了:天天向上
演员表演了:快乐大本营
观众观看了:快乐大本营
演员表演了:天天向上
观众观看了:天天向上
演员表演了:快乐大本营
观众观看了:快乐大本营
演员表演了:天天向上
观众观看了:天天向上
演员表演了:快乐大本营
观众观看了:快乐大本营
演员表演了:天天向上
观众观看了:天天向上
演员表演了:快乐大本营
观众观看了:快乐大本营
演员表演了:天天向上
观众观看了:天天向上

7.线程池

背景:经常创建和销毁线程,消耗特别大的资源,比如并发的情况下的线程,对性能影响很大。线程池就是问题为了解决这个问题,提前创建好多个线程,放在线程池中,使用时直接获取,使用完放回线程池中,可以避免频繁的创建、销毁,实现重复利用。

优点

  • 提高相应速度(减少创建线程的时间)
  • 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
  • 便于线程管理:corePoolSize:核心池的大小。maximumPoolSize:最大线程数。
    keepAliveTime:线程没有任务时最多保持多长时间后终止。

线程池相关的API:

  • ExecutorService:真正的线程池接口。

  • 常见的实现类:ThreadPoolExecutor。
    void execute(Runnable command):执行任务命令,没有返回值,一般用来执行Runnable.
    Future submit(Callable task):执行任务,有返回值,一般用来执行Callable

  • void shutdown():关闭连接池

  • Executors:工具类,线程池的工厂类,用来创建并返回不同类型的线程池

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

// 线程池
public class ThreadPool {
    public static void main(String[] args) {
        // 1、创建服务,创建线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        MyThread myThread = new MyThread();
        // 执行
        service.execute(myThread);
        service.execute(myThread);
        service.execute(myThread);
        service.execute(myThread);
        // 关闭连接
        service.shutdown();
    }
}

// 演员
class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}


//输出结果:
pool-1-thread-1
pool-1-thread-4
pool-1-thread-3
pool-1-thread-2

六.IO流

待续…

  • 27
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值