Java 注解在 Android 中的使用场景

Java 元注解有 5 种,常用的是 @Target 和 @Retention 两个。

其中 @Retention 表示保留级别,有三种:

  • RetentionPolicy.SOURCE - 标记的注解仅保留在源码级别中,并被编译器忽略
  • RetentionPolicy.CLASS - 标记的注解在编译时由编译器保留,但 JVM 会忽略
  • RetentionPolicy.RUNTIME - 标记的注解由 JVM 保留,因此运行时环境可以使用它

这三个值表示的生命周期为 SOURCE < CLASS < RUNTIME,即 RUNTIME 生命周期最长,涵盖了 SOURCE 和 CLASS,其次是 CLASS 的生命周期涵盖了 SOURCE。

不同的保留级别有不同的应用场景:

enter description here

1、源码级别应用场景

1.1 APT

Annotation Processor Tools,即注解处理器。它在由源文件(.java)被 javac 编译成为字节码文件(.class)这个过程中,先采集到所有的注解信息并封装到 Element 对象中,然后再由 javac 自动调用注解处理器,无须手动调用。

接下来对使用方法做一个简介。

先创建一个保留级别为源码级别的注解:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Frank {
    int value();
    String id();
}

然后创建一个 Java-Library 模块,来编写注解处理程序。模块的代码结构如下:

APT 模块结构

注册注解处理器

先注册注解处理器文件。只有注册后它才能被 javac 调用。注册方式有两种,一是按照上图的路径创建同名的文件夹和文件(路径名固定,不要自己发挥),然后在 javax.annotation.processing.Processor 文件中将注解处理器文件的全类名写在里面即可:

com.frank.compiler.MyProcessor

另一种方式就是使用 AutoService 自动生成。首先在模块 gradle 中加入依赖:

dependencies {
    implementation 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
}

AutoService 与 gradle-5.4.1 以上版本有兼容性问题,必须要向上边那样把 annotationProcessor 的依赖也写上才能避免无法自动生成 Processor 文件的情况。在 gradle-6.1.1 上亲测可行。

然后在注解处理器文件的类加上 AutoService 的注解:

@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}

AutoService 的值固定为 Processor.class。编译后在 module/build/classes/java/main/META-INF/services/ 目录下就能找到 javax.annotation.processing.Processor 文件了,其内容与手动构建的是一样的。

注解处理器使用

注解处理类需要继承 AbstractProcessor 才具有处理注解的能力,通过重写 process() 可以对注解进行处理:

@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Messager messager = processingEnv.getMessager();
        messager.printMessage(Diagnostic.Kind.NOTE, "注解处理器输出!");
        return false;
    }
}

以上只是简单的输出了一句 log,本节不会具体介绍注解处理器结合具体业务该如何编写,仅介绍一般的使用流程。

想要让这句 log 能打印出来,还需要在类名上边打上 @SupportedAnnotationTypes 注解,表示我们关心的注解类型,其值为我们要处理的注解的全类名,以我们前边的 @Frank 注解为例,就应该写成:

@AutoService(Processor.class)
@SupportedAnnotationTypes("com.example.myapplication.Frank")
public class MyProcessor extends AbstractProcessor {
    // .....
}

最后,在需要使用注解处理器的模块的 gradle 文件中,引入注解处理器模块 compiler:

dependencies {
    annotationProcessor project(':compiler')
}

这样在编译后就能看到 log 输出了:

注意输出是在执行 compileDebugJavaWithJavac 任务时输出的,再次说明了,注解处理器由 javac 自动调用并分析了其中的源码。也印证了 APT 是源码保留级别适用的技术。

1.2 IDE 语法检查

之所以叫 IDE 语法检查是因为,检测语法的是 IDE 工具(或其插件),并不是 Java 编译器。

在 Android 中我们需要设计接口以供使用者调用时,如出现需要对方法参数进行类型限定,如限定为资源 ID、布局 ID 等类型参数时,可以利用 Android 为我们提供的语法检查注解,来辅助进行更为直接的参数类型检查与提示。

在 androidx.annotation 包下有很多这样的注解,如 @DrawableRes、@IdRes、@LayoutRes 等等,想要使用它们需要先引入依赖:

    implementation 'androidx.annotation:annotation:1.2.0-alpha01'

使用时可以把它们加载方法类型之前:

    public final Drawable getDrawable(@DrawableRes int id) {
        return getResources().getDrawable(id, getTheme());
    }

这样在调用 getDrawable() 方法时,就必须传入一个 Drawable 的资源值,IDE 就会在实参位置有红色下划线提示,注意编译是不会报错的:

除了上述的注解之外,还有几个元注解 @IntDef、@LongDef、@StringDef,利用它们可以自定义类似于 @DrawableRes、@IdRes、@LayoutRes 这样的注解。典型应用就是用注解替代枚举以节省内存。

Java 中 Enum (枚举)的实质是特殊单例的静态成员变量,在运行期所有枚举类作为单例,全部加载到内存中。比常量多 5 到 10 倍的内存占用。每个枚举值,也就是枚举对象的构成为 12 个字节的对象头 + 所有成员变量,另外还有个 8 字节对齐。

@IntDef 的源码:

@Retention(SOURCE)
@Target({ANNOTATION_TYPE})
public @interface IntDef {
    /** Defines the allowed constants for this element */
    int[] value() default {};

    boolean flag() default false;

    boolean open() default false;
}

通过 value() 可以指定注解能包含哪些值。下面就利用这一点看看如何用注解替换枚举:

public class Gender {

    /*enum Gender {
        MALE, FEMALE
    }*/

    public static final int MALE = 0;
    public static final int FEMALE = 1;

    private int gender;

    @Retention(RetentionPolicy.SOURCE)
    @Target(ElementType.PARAMETER)
    @IntDef({MALE, FEMALE})
    @interface GenderInt {

    }

    public void setGender(@GenderInt int gender) {
        this.gender = gender;
    }
}

Gender 类表示性别,这里我们不用枚举,而是把男性和女性分别定义成两个整型常量 0 和 1。使用 @IntDef 注解自定义一个 @GenderInt 注解,用 @Target(ElementType.PARAMETER) 指定作用在参数上,用 @IntDef({MALE, FEMALE}) 规定可选值只有 MALE, FEMALE 两个。

在外部调用 setGender() 时,如果传入的值不在 MALE, FEMALE 之中,IDE 就会提醒:

使用源码级别注解的典型框架是 ARouter。关于 Android 组件化以及 ARouter 原理可以参考 Android 组件化基础(二)—— 仿 ARouter 实现一个路由框架。

2、字节码级别应用场景

字节码增强,也称为字节码插桩,说白了就是在字节码文件里写代码和修改。过程大概是:

.class 文件 -> 用 IO 流读写 class 文件 -> byte[] -> 按照 .class 文件格式进行修改。

这里仅举简单例子,不具体扩展。对字节码插桩感兴趣的可以去看 ASM 字节码插桩入门。

假如要统计每个方法的执行时间,并且不想在源代码的方法中加入时间统计代码,那就可以把需要统计的方法打上注解,编译之后在字节码的方法首末加入统计运行时间的代码:

添加 @InjectTime 注解

字节码文件插入统计时间代码

ARouter、ButterKnife 都是字节码保留级别的典型应用。

3、运行时级别应用场景

最典型的就是反射。

3.1 通过反射实现 findViewById()

一个最简单的示例就是通过反射 + 注解实现 findViewById()。

首先定义一个运行时级别的注解 @Inject:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
    int value();
}

值是被 @Inject 修饰控件的资源 ID。

然后编写工具类,拿到控件 ID 后,调用 findViewById() 得到控件对象,最后通过反射把对象设置给控件:

public class InjectUtils {

    public static void inject(Activity activity) {
        Class<? extends Activity> aClass = activity.getClass();
        Field[] fields = aClass.getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(Inject.class)) {
                Inject annotation = field.getAnnotation(Inject.class);
                int id = annotation.value();
                View view = activity.findViewById(id);
                field.setAccessible(true);
                try {
                    field.set(activity, view);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

给控件打上注解并标明资源 ID,进行测试:

public class MainActivity extends AppCompatActivity {

    @Inject(R.id.button)
    private Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectUtils.inject(this);
        button.setText("Inject Success");
    }
}

3.2 通过反射获取真实泛型类型

当我们对一个泛型类进行反射时,需要得到泛型中的真实数据类型,来完成如 Json 反序列化的操作。此时需要通过 Type 体系来完成。 Type 接口包含了一个实现类 Class 和四个实现接口,他们分别是:

  • TypeVariable 泛型类型变量,可以获取泛型上下限等信息
  • ParameterizedType 具体的泛型类型,可以获得元数据中泛型签名类型(泛型真实类型)
  • GenericArrayType 当需要描述的类型是泛型类的数组时,比如 List[]、Map[],此接口会作为 Type 的实现
  • WildcardType 通配符泛型,获得上下限信息

下面来介绍一个获取泛型真实类型的典型应用。

典型应用 —— Gson 反序列化

对从服务器接收的响应数据进行反序列化是项目中的常规操作。我们举个例子,假如使用 Gson 进行反序列化,用 Response 表示响应体,用 Data 表示 Response 中携带的真实数据:

public class Response<T> {

    private T data;
    private int code;
    private String message;

    public Response(T data, int code, String message) {
        this.data = data;
        this.code = code;
        this.message = message;
    }

    ...
}

public class Data {

    private String result;

    public Data(String result) {
        this.result = result;
    }

    @Override
    public String toString() {
        return "Data{" +
                "result='" + result + '\'' +
                '}';
    }
}

模拟从服务器接收数据进行反序列化的过程:

public class Test {

    public static void main(String[] args) {
        // 构造一个 Response 对象模拟服务器返回的数据
        Response<Data> response = new Response<>(new Data("数据"),1,"成功");
        Gson gson = new Gson();
        String json = gson.toJson(response);
        System.out.println(json);

        // 反序列化,由于没有提供 Response 内 data 的真实类型,所以反序列化失败抛出异常
        Response<Data> responseData = gson.fromJson(json, Response.class);
        System.out.println(responseData.getData().getClass());
    }
}

执行上述代码会抛出异常:

Exception in thread "main" java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to com.frank.reflect.Data
	at com.frank.reflect.Test.main(Test.java:16)

FAILURE: Build failed with an exception.

如果反序列化的 Bean 没有泛型,那么可以使用 public <T> T fromJson(String json, Class<T> classOfT) 方法进行反序列化,但如果像 Response 那样带有泛型,但是却没提供泛型的真实类型,就会抛出上面的异常。

解决方案是使用 TypeToken 提供 Response<Data> 的类型参数 Data 给 fromJson():

public class Test {

    public static void main(String[] args) {
        // 构造一个 Response 对象模拟服务器返回的数据
        Response<Data> response = new Response<>(new Data("数据"), 1, "成功");
        Gson gson = new Gson();
        String json = gson.toJson(response);
        System.out.println(json);

        // 反序列化,通过创建 TypeToken 的匿名子类获取到 Response 的真实类型,即 Data
        Type type = new TypeToken<Response<Data>>() {}.getType();
        Response<Data> responseData = gson.fromJson(json, type);
        System.out.println(responseData.getData().getClass());
    }
}

这样就能正确输出反序列化结果了:

{"data":{"result":"数据"},"code":1,"message":"成功"}
class com.frank.reflect.Data

解决问题的原理

为何通过匿名子类对象获取到类型参数?原因有二:

  1. Java 泛型在编译期被擦除,因此运行时无法直接获取到 Response<Data> 上的类型参数
  2. 匿名子类会保存类型参数。因为编译器会生成一个合成类来表示匿名子类的类型,并且在合成类中会保留泛型信息。这样做的目的是为了在匿名子类中能够正确地访问和使用泛型类型

基于以上两点,我们来看 Gson 源码是如何通匿名子类获取类型参数的:

public class TypeToken<T> {

  final Class<? super T> rawType;
  final Type type;
  final int hashCode;
  
  /**
  * 客户端创建一个空的匿名子类。这样做可以将类型参数嵌入到匿名类的类型层次结构中,
  * 因此我们可以在运行时重新构造它,尽管它被删除了
  */
  protected TypeToken() {
    // 1.对匿名子类调用 getSuperclassTypeParameter() 获取类型参数
    this.type = getSuperclassTypeParameter(getClass());
    this.rawType = (Class<? super T>) $Gson$Types.getRawType(type);
    this.hashCode = type.hashCode();
  }
  
  static Type getSuperclassTypeParameter(Class<?> subclass) {
    // 2.getGenericSuperclass() 会返回 subclass 所表示的实体(类、接口、原始类型或 void)的
    // 直接父类的类型,这个类型是带有泛型信息的
    Type superclass = subclass.getGenericSuperclass();
    if (superclass instanceof Class) {
      throw new RuntimeException("Missing type parameter.");
    }
    ParameterizedType parameterized = (ParameterizedType) superclass;
    // 3.取所有泛型类型的首个类型做规范化转换后返回
    return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);
  }
  
  public final Type getType() {
    return type;
  }
}

public final class $Gson$Types {

  public static Type canonicalize(Type type) {
    if (type instanceof Class) {
      Class<?> c = (Class<?>) type;
      return c.isArray() ? new GenericArrayTypeImpl(canonicalize(c.getComponentType())) : c;
    } else if (type instanceof ParameterizedType) {
      // 如果是一个参数化类型就返回一个 ParameterizedTypeImpl 的实例
      ParameterizedType p = (ParameterizedType) type;
      return new ParameterizedTypeImpl(p.getOwnerType(),
          p.getRawType(), p.getActualTypeArguments());
    } else if (type instanceof GenericArrayType) {
      GenericArrayType g = (GenericArrayType) type;
      return new GenericArrayTypeImpl(g.getGenericComponentType());
    } else if (type instanceof WildcardType) {
      WildcardType w = (WildcardType) type;
      return new WildcardTypeImpl(w.getUpperBounds(), w.getLowerBounds());
    } else {
      // type is either serializable as-is or unsupported
      return type;
    }
  }
}

实际上 TypeToken 构造方法上,官方给的注释已经提示了使用匿名子类的原因。具体的实现步骤可以分三步:

  1. 将 TypeToken 的构造方法声明为 protected,也就是说,在 TypeToken 所在的 com.google.gson.reflect 包之外,想要拿到 TypeToken 或其子类对象,就无法通过 new TypeToken() 的方法,而只能使用创建匿名子类对象的方式,即 new TypeToken(){}。getClass() 拿到的就是这个匿名子类:class com.frank.reflect.Test$1(在 Test 内声明的匿名子类),然后对这个匿名子类调用 getSuperclassTypeParameter()
  2. 在 getSuperclassTypeParameter() 内先对匿名子类调用 getGenericSuperclass() 获取到带有泛型信息的父类,即 TypeToken<Response<Data>>
  3. 将父类对象转换为 ParameterizedType,并调用其 getActualTypeArguments() 获取到真实的类型参数数组,取数组的第 0 个(在这个例子中就是 Response<Data>)传入 G s o n Gson GsonTypes.canonicalize() 将结果进行规范化并返回

TypeToken 设计的巧妙之处就在于用 protected 的构造方法迫使你使用匿名子类才能构建其对象,然后在 getSuperclassTypeParameter() 方法中用这个匿名子类对象对应的 Class 去获取含有泛型的直接父类,就是 TypeToken<T>。使用匿名子类获取真实类型参数的做法也很值得借鉴。

4、练习

通过反射 + 注解实现被启动 Activity 自动获取 Intent 中携带的数据。详细点说,就是在 Activity1 通过 Intent 启动 Activity2 时,可能会给 Intent 通过 putXxxExtra() 的形式附带一些参数。在 Activity2 中拿到 Intent 对象之后需要一个一个地把数据从 Intent 中拿出来,过程繁琐。我们要优化的就是从 Intent 中手动拿数据这个过程。

首先在 MainActivity 中建立一些要传递的数据,包含多种类型:

public class MainActivity extends AppCompatActivity {

    // 需要作为 Intent 的参数被传递的变量要提出来做成员变量
    private String string;
    private String attr;
    private int[] array;
    private UserParcelable userParcelable;
    private UserParcelable[] userParcelables;
    private List<UserParcelable> userParcelableList;
    private UserSerializable userSerializable;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Intent intent = new Intent(MainActivity.this, SecondActivity.class);
        ArrayList<Parcelable> parcelables = new ArrayList<>();
        parcelables.add(new UserParcelable("user3"));
        intent.putExtra("StringKey", "testString")
                .putExtra("attr", "attr")
                .putExtra("array", new int[]{1, 2})
                .putExtra("userParcelable", new UserParcelable("cat"))
                .putExtra("userParcelables", new UserParcelable[]{new UserParcelable("user1"),
                        new UserParcelable("user2")})
                .putExtra("userParcelableList", parcelables)
                .putExtra("userSerializable", new UserSerializable("user4"));

        startActivity(intent);
    }
}

其中 UserParcelable 与 UserSerializable 两个类型分别模拟实现了 Parcelable 和 Serializable 接口的 JavaBean 类型:

public class UserParcelable implements Parcelable {

    String name;

    public UserParcelable(String name) {
        this.name = name;
    }

    protected UserParcelable(Parcel in) {
        name = in.readString();
    }

    public static final Creator<UserParcelable> CREATOR = new Creator<UserParcelable>() {
        @Override
        public UserParcelable createFromParcel(Parcel in) {
            return new UserParcelable(in);
        }

        @Override
        public UserParcelable[] newArray(int size) {
            return new UserParcelable[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
    }
}
------------------------------------------------------------------
public class UserSerializable implements Serializable {

    String name;

    public UserSerializable(String name) {
        this.name = name;
    }
}

然后新建一个注解 @InjectExtras:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectExtras {
    String value() default "";
}

用法是标记接收 Intent 数据一方的成员变量,value() 默认为空,为空时说明 MainActivity 在存数据的时候,key 和变量名是一样的,如果不一样,就把 key 作为 value:

public class SecondActivity extends AppCompatActivity {

    @InjectExtras("StringKey")
    private String string;

    @InjectExtras
    private String attr;

    @InjectExtras
    private int[] array;

    @InjectExtras
    private UserParcelable userParcelable;

    @InjectExtras("userParcelables")
    private UserParcelable[] userParcelables;

    @InjectExtras
    private List<UserParcelable> userParcelableList;

    @InjectExtras
    private UserSerializable userSerializable;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        InjectUtils.injectExtra(this);
    }
}

最后实现工具方法:

	/**
     * 拿到 activity 后,遍历所有成员变量,筛选出被 @InjectExtras 注释的成员,
     * 获得其对应的属性 Key 值,然后通过 getIntent().getExtras() 获取到对方
     * 发送的参数 Bundle,然后根据 Key 从中取出 Value,最后通过反射的方式为接
     * 收方的成员变量赋值。
     *
     * @param activity 接收参数的 Activity
     */
    public static void injectExtra(Activity activity) {
        Class<? extends Activity> clazz = activity.getClass();
        Field[] fields = clazz.getDeclaredFields();
        Bundle extras = activity.getIntent().getExtras();

        for (Field field : fields) {
            if (field.isAnnotationPresent(InjectExtras.class)) {
                InjectExtras annotation = field.getAnnotation(InjectExtras.class);

                if (annotation == null) {
                    Log.e(TAG, "Get annotation @InjectExtras failed.");
                    return;
                }

                // 找到 field 在发送方存入参数的 key,并取出对应的 value
                String key = TextUtils.isEmpty(annotation.value()) ? field.getName() : annotation.value();
                Object object = extras.get(key);

                // value 如果是 Parcelable[] 则不能直接赋值,其它类型可以,所以通过 getComponentType()
                // 获取数组元素类型,如果不是数组则返回 null
                Class<?> componentType = field.getType().getComponentType();
                if (componentType != null && field.getType().isArray() && Parcelable.class.isAssignableFrom(componentType)) {
                    // 确定了 object 是 Parcelable[],进行转换
                    Object[] objArray = (Object[]) object;
                    // 使用工具类将数组内元素转换成 field 对应的类型,即 UserParcelable
                    // 第三个参数实际上要获取的是 UserParcelable[].class 这个类型,只能通过反射获取
                    object = Arrays.copyOf(objArray, objArray.length, (Class<? extends Object[]>) field.getType());
                }

                field.setAccessible(true);
                try {
                    field.set(activity, object);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

获取 Extra 中 key 的规则是,如果 Field 被 @InjectExtras 注解指定了 value,那么就用指定的值,否则默认使用变量名。

拿到 key 之后获取对应的 value,如果 value 类型不是 Parcelable[] 的话就可以直接设置给对应的 Field 了;如果是的话,需要进行一个转化再赋值。这一步中需要注意,如果 field 是 UserParcelable[],那么 field.getType() 得到的是 class [Lxxx.xxx.UserParcelable,再调用 getComponentType() 就拿到了数组内的元素类型 class xxx.xxx.UserParcelable。由于不能将 object 直接做类型强转为 Parcelable[],所以借用 Arrays.copyOf() 做这个类型转换,第三个参数 field.getType() 的结果不能直接用,编译不过,需要将 class [Lxxx.xxx.UserParcelable 转换为 Class<? extends Object[]>,因为 field.getType() 返回的类型实际上是一个 Class<?>,强转成 Class<? extends UserParcelable[]> 会有 unchecked warning,但是转成 Class<? extends Object[]> 就不会。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/191691.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Springboot系列】SpringBoot整合WebSocket,既然如此简单(含源码)

文章目录 前言&#xff1a;什么是WebSocket&#xff1f;Spring Boot中的WebSocket支持WebSocket和HTTP优劣势WebSocket的优势&#xff1a;1.实时性&#xff1a;2.较低的延迟&#xff1a;3.较小的数据传输量&#xff1a;4.更好的兼容性&#xff1a; HTTP的优势&#xff1a;1.简单…

从0开始学习JavaScript--JavaScript中的对象原型

JavaScript中的对象原型是理解该语言核心概念的关键之一。本文将深入探讨JavaScript对象原型的作用、使用方法以及与继承相关的重要概念。通过详细的示例代码和全面的讲解&#xff0c;将能够更好地理解和运用JavaScript对象原型&#xff0c;提高代码的可维护性和扩展性。 Java…

RHCE---给openlab搭建web网站

作业&#xff1a;请给openlab搭建web网站 网站需求&#xff1a; 1.基于域名 www.openlab.com 可以访问网站内容为 welcome to openlab!!! 2.给该公司创建三个子界面分别显示学生信息&#xff0c;教学资料和缴费网站&#xff0c; 1、基于 www.openlab.com/student 网站访问学生信…

C#,《小白学程序》第十八课:随机数(Random)第五,方差及标准方差(标准差)的计算方法与代码

1 文本格式 /// <summary> /// 《小白学程序》第十八课&#xff1a;随机数&#xff08;Random&#xff09;第五&#xff0c;方差及标准方差&#xff08;标准差&#xff09;的计算方法与代码 /// 方差 SUM(&#xff08;Xi - X)^2 ) / n i0...n-1 X Average of X[i] ///…

如果客户端同时有ipv4和ipv6,浏览器是如何选择用哪种ip

在互联网协议&#xff08;IP&#xff09;的发展历程中&#xff0c;IPv4和IPv6是两种主要的版本。对于一个客户端来说&#xff0c;同时拥有IPv4和IPv6的能力是常见的情况。那么&#xff0c;当一个客户端同时具有IPv4和IPv6的能力时&#xff0c;浏览器是如何选择使用哪种IP进行通…

redis(Remote Dictionary Service) 底层数据结构

redis 底层数据结构 动态字符串SDS 优点 获取字符串长度的时间复杂度O(1) 支持动态扩容&#xff0c;减少内存分配次数 新字符串小于1M – 新空间为扩展后字符串长度的两倍 1 新字符串大于1M – 新空间为扩展后字符串长度 1M 1. 内存预分配 二进制安全&#xff08;记录了…

高并发系统:它的通用设计方法是什么?

Java全能学习面试指南&#xff1a;https://javaxiaobear.cn 我们知道&#xff0c;高并发代表着大流量&#xff0c;高并发系统设计的魅力就在于我们能够凭借自己的聪明才智设计巧妙的方案&#xff0c;从而抵抗巨大流量的冲击&#xff0c;带给用户更好的使用体验。这些方案好似能…

Kerberos 高可用配置和验证

参考 https://cloud.tencent.com/developer/article/1078314 https://mp.weixin.qq.com/s?__bizMzI4OTY3MTUyNg&mid2247485861&idx1&snbb930a497f63ac5e63ed20c64643eec5 机器准备 Kerberos主 ip-172-31-22-86.ap-southeast-1.compute.internal 7.common2.hado…

【洛谷算法题】P5715-三位数排序【入门2分支结构】

&#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P5715-三位数排序【入门2分支结构】&#x1f30f;题目描述&#x1f30f;输入格式…

Python----函数的数据 拆包(元组和字典)

Python拆包&#xff1a; 就是把元组或字典中的数据单独的拆分出来&#xff0c;然后赋予给其他的变量。 拆包: 对于函数中的多个返回数据, 去掉 元组, 列表 或者字典 直接获取里面数据的过程。 元组的拆包过程 def func():# 经过一系列操作返回一个元组return 100, 200 …

100天精通Python(可视化篇)——第109天:Pyecharts绘制各种常用地图(参数说明+代码实战)

文章目录 专栏导读一、地图应用场景二、参数说明1. 导包2. add函数 三、地图绘制实战1. 省市地图2. 中国地图3. 中国地图&#xff08;带城市&#xff09;4. 中国地图&#xff08;分段型&#xff09;5. 中国地图&#xff08;连续型&#xff09;6. 世界地图7. 行程轨迹地图8. 人口…

LCR 047. 二叉树剪枝 和 leetCode 1110. 删点成林 + 递归 + 图解

给定一个二叉树 根节点 root &#xff0c;树的每个节点的值要么是 0&#xff0c;要么是 1。请剪除该二叉树中所有节点的值为 0 的子树。节点 node 的子树为 node 本身&#xff0c;以及所有 node 的后代。 示例 1: 输入: [1,null,0,0,1] 输出: [1,null,0,null,1] 解释: 只有红…

【古诗生成AI实战】之一——实战项目总览

[1] 总览 【古诗生成AI实战】系列共五篇文章&#xff1a; 【古诗生成AI实战】之一——实战项目总览   【古诗生成AI实战】之二——项目架构设计   【古诗生成AI实战】之三——任务加载器与预处理器   【古诗生成AI实战】之四——模型包装器与模型的训练   【古诗生成AI…

rust tokio select!宏详解

rust tokio select!宏详解 简介 本文介绍Tokio中select!的用法&#xff0c;重点是使用过程中可能遇到的问题&#xff0c;比如阻塞问题、优先级问题、cancel safe问题。在Tokio 中&#xff0c;select! 是一个宏&#xff0c;用于同时等待多个异步任务&#xff0c;并在其中任意一…

MySQL简单介绍

简单了解MySQL MySQL语句分类 SQL语句分类 DDL&#xff1a;数据定义语句 create表&#xff0c;库.….] DML&#xff1a;数据操作语句 [增加insert&#xff0c;修改 update&#xff0c;删除delete] DQL&#xff1a;数据查询语句 [select] DCL&#xff1a;数据控制语句 …

【RTP】3: RTPSenderVideo::SendVideo 切片到发送

m98 版本。之前1 2 都是m79.RTPSenderVideo::SendVideo 负责切片,是入口 实际发送要靠: RTPSender* const rtp_sender_; 外部传递的: rtp_rtcp\source\rtp_sender.h 实现了rtp rtcp协议 ,负责实际的打包 新增了一个 TransformableFrameInterface 用的 编码线程 - RTPSend…

【算法萌新闯力扣】:卡牌分组

力扣热题&#xff1a;卡牌分组 一、开篇 今天是备战蓝桥杯的第22天。这道题触及到我好几个知识盲区&#xff0c;以前欠下的债这道题一并补齐&#xff0c;哈希表的遍历、最大公约数与最小公倍数&#xff0c;如果你还没掌握&#xff0c;这道题练起来&#xff01; 二、题目链接:…

关于el-table的二次封装及使用,支持自定义列内容

关于el-table的二次封装及使用 table组件 <template><el-table ref"tableComRef" :data"tableData" border style"width: 100%"><el-table-column v-if"tableHeaderList[0]?.type selection" type"selection&…

下载网页内容成HTML文件

今天遇到了一个非常好用的、开源的网页下载插件: SingleFile&#xff0c;它可以将当前网页里的文字、图片、超链接等&#xff0c;合并成单一的.html文件&#xff0c;便于保存和浏览查看。下面介绍SingleFile的安装和使用。 1、下载SingleFile插件 SingleFile官网地址&#xff…

如何使用JMeter测试导入接口/导出接口

今天一上班&#xff0c;被开发问了一个问题&#xff1a;JMeter调试接口&#xff0c;文件导入接口怎么老是不通&#xff1f;还有导出文件接口&#xff0c;不知道文件导到哪里去了&#xff1f; 我一听&#xff0c;这不是JMeter做接口测试经常遇到的嘛&#xff0c;但是一时半会又…