JavaSE笔记(七)

news2025/1/19 14:59:03

Java反射和注解

**注意:**本章节涉及到JVM相关底层原理,难度会有一些大。

反射就是把Java类中的各个成分映射成一个个的Java对象。即在运行状态中,对于任意一个类,都能够知道这个类所有的属性和方法,对于任意一个对象,都能调用它的任意一个方法和属性。这种动态获取信息及动态调用对象方法的功能叫Java的反射机制。

简而言之,我们可以通过反射机制,获取到类的一些属性,包括类里面有哪些字段,有哪些方法,继承自哪个类,甚至还能获取到泛型!它的权限非常高,慎重使用!

Java类加载机制

在学习Java的反射机制之前,我们需要先了解一下类的加载机制,一个类是如何被加载和使用的:

img

在Java程序启动时,JVM会将一部分类(class文件)先加载(并不是所有的类都会在一开始加载),通过ClassLoader将类加载,在加载过程中,会将类的信息提取出来(存放在元空间中,JDK1.8之前存放在永久代),同时也会生成一个Class对象存放在内存(堆内存),注意此Class对象只会存在一个,与加载的类唯一对应!

**思考:**既然说和与加载的类唯一对应,那如果我们手动创建一个与JDK包名一样,同时类名也保持一致,那么JVM会加载这个类吗?

package java.lang;

public class String {    //JDK提供的String类也是
    public static void main(String[] args) {
        System.out.println("我姓🐴,我叫🐴nb");
    }
}

我们发现,会出现以下报错:

错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)

但是我们明明在自己写的String类中定义了main方法啊,为什么会找不到此方法呢?实际上这是ClassLoader的双亲委派机制在保护Java程序的正常运行:

img

实际上我们的类最开始是由BootstarpClassLoader进行加载,BootstarpClassLoader用于加载JDK提供的类,而我们自己编写的类实际上是AppClassLoader,只有BootstarpClassLoader都没有加载的类,才会让AppClassLoader来加载,因此我们自己编写的同名包同名类不会被加载,而实际要去启动的是真正的String类,也就自然找不到main方法了!

public class Main {
    public static void main(String[] args) {
        System.out.println(Main.class.getClassLoader());   //查看当前类的类加载器
        System.out.println(Main.class.getClassLoader().getParent());  //父加载器
        System.out.println(Main.class.getClassLoader().getParent().getParent());  //爷爷加载器
        System.out.println(String.class.getClassLoader());   //String类的加载器
    }
}

由于BootstarpClassLoader是C++编写的,我们在Java中是获取不到的。

Class对象

通过前面,我们了解了类的加载,同时会提取一个类的信息生成Class对象存放在内存中,而反射机制其实就是利用这些存放的类信息,来获取类的信息和操作类。那么如何获取到每个类对应的Class对象呢,我们可以通过以下方式:

public static void main(String[] args) throws ClassNotFoundException {
    Class<String> clazz = String.class;   //使用class关键字,通过类名获取
    Class<?> clazz2 = Class.forName("java.lang.String");   //使用Class类静态方法forName(),通过包名.类名获取,注意返回值是Class<?>
    Class<?> clazz3 = new String("cpdd").getClass();  //通过实例对象获取
}

注意Class类也是一个泛型类,只有第一种方法,能够直接获取到对应类型的Class对象,而以下两种方法使用了?通配符作为返回值,但是实际上都和第一个返回的是同一个对象:

Class<String> clazz = String.class;   //使用class关键字,通过类名获取
Class<?> clazz2 = Class.forName("java.lang.String");   //使用Class类静态方法forName(),通过包名.类名获取,注意返回值是Class<?>
Class<?> clazz3 = new String("cpdd").getClass();

System.out.println(clazz == clazz2);
System.out.println(clazz == clazz3);

通过比较,验证了我们一开始的结论,在JVM中每个类始终只存在一个Class对象,无论通过什么方法获取,都是一样的。现在我们再来看看这个问题:

public static void main(String[] args) {
    Class<?> clazz = int.class;   //基本数据类型有Class对象吗?
    System.out.println(clazz);
}

迷了,不是每个类才有Class对象吗,基本数据类型又不是类,这也行吗?实际上,基本数据类型也有对应的Class对象(反射操作可能需要用到),而且我们不仅可以通过class关键字获取,其实本质上是定义在对应的包装类中的:

/**
 * The {@code Class} instance representing the primitive type
 * {@code int}.
 *
 * @since   JDK1.1
 */
@SuppressWarnings("unchecked")
public static final Class<Integer>  TYPE = (Class<Integer>) Class.getPrimitiveClass("int");

/*
 * Return the Virtual Machine's Class object for the named
 * primitive type
 */
static native Class<?> getPrimitiveClass(String name);   //C++实现,并非Java定义

每个包装类中(包括Void),都有一个获取原始类型Class方法,注意,getPrimitiveClass获取的是原始类型,并不是包装类型,只是可以使用包装类来表示。

public static void main(String[] args) {
    Class<?> clazz = int.class;
    System.out.println(Integer.TYPE == int.class);
}

通过对比,我们发现实际上包装类型都有一个TYPE,其实也就是基本类型的Class,那么包装类的Class和基本类的Class一样吗?

public static void main(String[] args) {
    System.out.println(Integer.TYPE == Integer.class);
}

我们发现,包装类型的Class对象并不是基本类型Class对象。数组类型也是一种类型,只是编程不可见,因此我们可以直接获取数组的Class对象:

public static void main(String[] args) {
    Class<String[]> clazz = String[].class;
    System.out.println(clazz.getName());  //获取类名称(得到的是包名+类名的完整名称)
    System.out.println(clazz.getSimpleName());
    System.out.println(clazz.getTypeName());
    System.out.println(clazz.getClassLoader());   //获取它的类加载器
    System.out.println(clazz.cast(new Integer("10")));   //强制类型转换
}

再谈instanceof

正常情况下,我们使用instanceof进行类型比较:

public static void main(String[] args) {
    String str = "";
    System.out.println(str instanceof String);
}

它可以判断一个对象是否为此接口或是类的实现或是子类,而现在我们有了更多的方式去判断类型:

public static void main(String[] args) {
    String str = "";
    System.out.println(str.getClass() == String.class);   //直接判断是否为这个类型
}

如果需要判断是否为子类或是接口/抽象类的实现,我们可以使用asSubClass()方法:

public static void main(String[] args) {
    Integer i = 10;
    i.getClass().asSubclass(Number.class);   //当Integer不是Number的子类时,会产生异常
}

获取父类信息

通过getSuperclass()方法,我们可以获取到父类的Class对象:

public static void main(String[] args) {
    Integer i = 10;
    System.out.println(i.getClass().getSuperclass());
}

也可以通过getGenericSuperclass()获取父类的原始类型的Type:

public static void main(String[] args) {
    Integer i = 10;
    Type type = i.getClass().getGenericSuperclass();
    System.out.println(type);
    System.out.println(type instanceof Class);
}

我们发现Type实际上是Class类的父接口,但是获取到的Type的实现并不一定是Class。

同理,我们也可以像上面这样获取父接口:

public static void main(String[] args) {
    Integer i = 10;
    for (Class<?> anInterface : i.getClass().getInterfaces()) {
        System.out.println(anInterface.getName());
    }
  
  	for (Type genericInterface : i.getClass().getGenericInterfaces()) {
        System.out.println(genericInterface.getTypeName());
    }
}

创建类对象

既然我们拿到了类的定义,那么是否可以通过Class对象来创建对象、调用方法、修改变量呢?当然是可以的,那我们首先来探讨一下如何创建一个类的对象:

public static void main(String[] args) throws InstantiationException, IllegalAccessException {
    Class<Student> clazz = Student.class;
    Student student = clazz.newInstance();
    student.test();
}

static class Student{
    public void test(){
        System.out.println("萨日朗");
    }
}

通过使用newInstance()方法来创建对应类型的实例,返回泛型T,注意它会抛出InstantiationException和IllegalAccessException异常,那么什么情况下会出现异常呢?

public static void main(String[] args) throws InstantiationException, IllegalAccessException {
    Class<Student> clazz = Student.class;
    Student student = clazz.newInstance();
    student.test();
}

static class Student{

    public Student(String text){
        
    }

    public void test(){
        System.out.println("萨日朗");
    }
}

当类默认的构造方法被带参构造覆盖时,会出现InstantiationException异常,因为newInstance()只适用于默认无参构造。

public static void main(String[] args) throws InstantiationException, IllegalAccessException {
    Class<Student> clazz = Student.class;
    Student student = clazz.newInstance();
    student.test();
}

static class Student{

    private Student(){}

    public void test(){
        System.out.println("萨日朗");
    }
}

当默认无参构造的权限不是public时,会出现IllegalAccessException异常,表示我们无权去调用默认构造方法。在JDK9之后,不再推荐使用newInstance()方法了,而是使用我们接下来要介绍到的,通过获取构造器,来实例化对象:

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    Class<Student> clazz = Student.class;
    Student student = clazz.getConstructor(String.class).newInstance("what's up");
    student.test();
}

static class Student{

    public Student(String str){}

    public void test(){
        System.out.println("萨日朗");
    }
}

通过获取类的构造方法(构造器)来创建对象实例,会更加合理,我们可以使用getConstructor()方法来获取类的构造方法,同时我们需要向其中填入参数,也就是构造方法需要的类型,当然我们这里只演示了。那么,当访问权限不是public的时候呢?

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    Class<Student> clazz = Student.class;
    Student student = clazz.getConstructor(String.class).newInstance("what's up");
    student.test();
}

static class Student{

    private Student(String str){}

    public void test(){
        System.out.println("萨日朗");
    }
}

我们发现,当访问权限不足时,会无法找到此构造方法,那么如何找到非public的构造方法呢?

Class<Student> clazz = Student.class;
Constructor<Student> constructor = clazz.getDeclaredConstructor(String.class);
constructor.setAccessible(true);   //修改访问权限
Student student = constructor.newInstance("what's up");
student.test();

使用getDeclaredConstructor()方法可以找到类中的非public构造方法,但是在使用之前,我们需要先修改访问权限,在修改访问权限之后,就可以使用非public方法了(这意味着,反射可以无视权限修饰符访问类的内容)


调用类的方法

我们可以通过反射来调用类的方法(本质上还是类的实例进行调用)只是利用反射机制实现了方法的调用,我们在包下创建一个新的类:

package com.test;

public class Student {
    public void test(String str){
        System.out.println("萨日朗"+str);
    }
}

这次我们通过forName(String)来找到这个类并创建一个新的对象:

public static void main(String[] args) throws ReflectiveOperationException {
    Class<?> clazz = Class.forName("com.test.Student");
    Object instance = clazz.newInstance();   //创建出学生对象
    Method method = clazz.getMethod("test", String.class);   //通过方法名和形参类型获取类中的方法
    
    method.invoke(instance, "what's up");   //通过Method对象的invoke方法来调用方法
}

通过调用getMethod()方法,我们可以获取到类中所有声明为public的方法,得到一个Method对象,我们可以通过Method对象的invoke()方法(返回值就是方法的返回值,因为这里是void,返回值为null)来调用已经获取到的方法,注意传参。

我们发现,利用反射之后,在一个对象从构造到方法调用,没有任何一处需要引用到对象的实际类型,我们也没有导入Student类,整个过程都是反射在代替进行操作,使得整个过程被模糊了,过多的使用反射,会极大地降低后期维护性。

同构造方法一样,当出现非public方法时,我们可以通过反射来无视权限修饰符,获取非public方法并调用,现在我们将test()方法的权限修饰符改为private:

public static void main(String[] args) throws ReflectiveOperationException {
    Class<?> clazz = Class.forName("com.test.Student");
    Object instance = clazz.newInstance();   //创建出学生对象
    Method method = clazz.getDeclaredMethod("test", String.class);   //通过方法名和形参类型获取类中的方法
    method.setAccessible(true);

    method.invoke(instance, "what's up");   //通过Method对象的invoke方法来调用方法
}

Method和Constructor都和Class一样,他们存储了方法的信息,包括方法的形式参数列表,返回值,方法的名称等内容,我们可以直接通过Method对象来获取这些信息:

public static void main(String[] args) throws ReflectiveOperationException {
    Class<?> clazz = Class.forName("com.test.Student");
    Method method = clazz.getDeclaredMethod("test", String.class);   //通过方法名和形参类型获取类中的方法
    
    System.out.println(method.getName());   //获取方法名称
    System.out.println(method.getReturnType());   //获取返回值类型
}

当方法的参数为可变参数时,我们该如何获取方法呢?实际上,我们在之前就已经提到过,可变参数实际上就是一个数组,因此我们可以直接使用数组的class对象表示:

Method method = clazz.getDeclaredMethod("test", String[].class);

反射非常强大,尤其是我们提到的越权访问,但是请一定谨慎使用,别人将某个方法设置为private一定有他的理由,如果实在是需要使用别人定义为private的方法,就必须确保这样做是安全的,在没有了解别人代码的整个过程就强行越权访问,可能会出现无法预知的错误。


修改类的属性

我们还可以通过反射访问一个类中定义的成员字段也可以修改一个类的对象中的成员字段值,通过getField()方法来获取一个类定义的指定字段:

public static void main(String[] args) throws ReflectiveOperationException {
    Class<?> clazz = Class.forName("com.test.Student");
    Object instance = clazz.newInstance();

    Field field = clazz.getField("i");   //获取类的成员字段i
    field.set(instance, 100);   //将类实例instance的成员字段i设置为100

    Method method = clazz.getMethod("test");
    method.invoke(instance);
}

在得到Field之后,我们就可以直接通过set()方法为某个对象,设定此属性的值,比如上面,我们就为instance对象设定值为100,当访问private字段时,同样可以按照上面的操作进行越权访问:

public static void main(String[] args) throws ReflectiveOperationException {
    Class<?> clazz = Class.forName("com.test.Student");
    Object instance = clazz.newInstance();

    Field field = clazz.getDeclaredField("i");   //获取类的成员字段i
    field.setAccessible(true);
    field.set(instance, 100);   //将类实例instance的成员字段i设置为100

    Method method = clazz.getMethod("test");
    method.invoke(instance);
}

现在我们已经知道,反射几乎可以把一个类的老底都给扒出来,任何属性,任何内容,都可以被反射修改,无论权限修饰符是什么,那么,如果我的字段被标记为final呢?现在在字段i前面添加final关键字,我们再来看看效果:

private final int i = 10;

这时,当字段为final时,就修改失败了!当然,通过反射可以直接将final修饰符直接去除,去除后,就可以随意修改内容了,我们来尝试修改Integer的value值:

public static void main(String[] args) throws ReflectiveOperationException {
    Integer i = 10;

    Field field = Integer.class.getDeclaredField("value");

    Field modifiersField = Field.class.getDeclaredField("modifiers");  //这里要获取Field类的modifiers字段进行修改
    modifiersField.setAccessible(true);
    modifiersField.setInt(field,field.getModifiers()&~Modifier.FINAL);  //去除final标记

    field.setAccessible(true);
    field.set(i, 100);   //强行设置值

    System.out.println(i);
}

我们可以发现,反射非常暴力,就连被定义为final字段的值都能强行修改,几乎能够无视一切阻拦。我们来试试看修改一些其他的类型:

public static void main(String[] args) throws ReflectiveOperationException {
    List<String> i = new ArrayList<>();

    Field field = ArrayList.class.getDeclaredField("size");
    field.setAccessible(true);
    field.set(i, 10);

    i.add("测试");   //只添加一个元素
    System.out.println(i.size());  //大小直接变成11
    i.remove(10);   //瞎移除都不带报错的,淦
}

实际上,整个ArrayList体系由于我们的反射操作,导致被破坏,因此它已经无法正常工作了!

再次强调,在进行反射操作时,必须注意是否安全,虽然拥有了创世主的能力,但是我们不能滥用,我们只能把它当做一个不得已才去使用的工具!


自定义ClassLoader加载类

我们可以自己手动将class文件加载到JVM中吗?先写好我们定义的类:

package com.test;

public class Test {
    public String text;

    public void test(String str){
        System.out.println(text+" > 我是测试方法!"+str);
    }
}

通过javac命令,手动编译一个.class文件:

nagocoler@NagodeMacBook-Pro HelloWorld % javac src/main/java/com/test/Test.java

编译后,得到一个class文件,我们把它放到根目录下,然后编写一个我们自己的ClassLoader,因为普通的ClassLoader无法加载二进制文件,因此我们编写一个自己的来让它支持:

//定义一个自己的ClassLoader
static class MyClassLoader extends ClassLoader{
    public Class<?> defineClass(String name, byte[] b){
        return defineClass(name, b, 0, b.length);   //调用protected方法,支持载入外部class文件
    }
}

public static void main(String[] args) throws IOException {
    MyClassLoader classLoader = new MyClassLoader();
    FileInputStream stream = new FileInputStream("Test.class");
    byte[] bytes = new byte[stream.available()];
    stream.read(bytes);
    Class<?> clazz = classLoader.defineClass("com.test.Test", bytes);   //类名必须和我们定义的保持一致
    System.out.println(clazz.getName());   //成功加载外部class文件
}

现在,我们就将此class文件读取并解析为Class了,现在我们就可以对此类进行操作了(注意,我们无法在代码中直接使用此类型,因为它是我们直接加载的),我们来试试看创建一个此类的对象并调用其方法:

try {
    Object obj = clazz.newInstance();
    Method method = clazz.getMethod("test", String.class);   //获取我们定义的test(String str)方法
    method.invoke(obj, "哥们这瓜多少钱一斤?");
}catch (Exception e){
    e.printStackTrace();
}

我们来试试看修改成员字段之后,再来调用此方法:

try {
    Object obj = clazz.newInstance();
    Field field = clazz.getField("text");   //获取成员变量 String text;
    field.set(obj, "华强");
    Method method = clazz.getMethod("test", String.class);   //获取我们定义的test(String str)方法
    method.invoke(obj, "哥们这瓜多少钱一斤?");
}catch (Exception e){
    e.printStackTrace();
}

通过这种方式,我们就可以实现外部加载甚至是网络加载一个类,只需要把类文件传递即可,这样就无需再将代码写在本地,而是动态进行传递,不仅可以一定程度上防止源代码被反编译(只是一定程度上,想破解你代码有的是方法),而且在更多情况下,我们还可以对byte[]进行加密,保证在传输过程中的安全性。


注解

其实我们在之前就接触到注解了,比如@Override表示重写父类方法(当然不加效果也是一样的,此注解在编译时会被自动丢弃)注解本质上也是一个类,只不过它的用法比较特殊。

注解可以被标注在任意地方,包括方法上、类名上、参数上、成员属性上、注解定义上等,就像注释一样,它相当于我们对某样东西的一个标记。而与注释不同的是,注解可以通过反射在运行时获取,注解也可以选择是否保留到运行时。

预设注解

JDK预设了以下注解,作用于代码:

  • @Override - 检查(仅仅是检查,不保留到运行时)该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
  • @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
  • @SuppressWarnings - 指示编译器去忽略注解中声明的警告(仅仅编译器阶段,不保留到运行时)
  • @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
  • @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。

元注解

元注解是作用于注解上的注解,用于我们编写自定义的注解:

  • @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
  • @Documented - 标记这些注解是否包含在用户文档中。
  • @Target - 标记这个注解应该是哪种 Java 成员。
  • @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
  • @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。

看了这么多预设的注解,你们肯定眼花缭乱了,那我们来看看@Override是如何定义的:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

该注解由@Target限定为只能作用于方法上,ElementType是一个枚举类型,用于表示此枚举的作用域,一个注解可以有很多个作用域。@Retention表示此注解的保留策略,包括三种策略,在上述中有写到,而这里定义为只在代码中。一般情况下,自定义的注解需要定义1个@Retention和1-n个@Target

既然了解了元注解的使用和注解的定义方式,我们就来尝试定义一个自己的注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
}

这里我们定义一个Test注解,并将其保留到运行时,同时此注解可以作用于方法或是类上:

@Test
public class Main {
    @Test
    public static void main(String[] args) {
        
    }
}

这样,一个最简单的注解就被我们创建了。

注解的使用

我们还可以在注解中定义一些属性,注解的属性也叫做成员变量,注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    String value();
}

默认只有一个属性时,我们可以将其名字设定为value,否则,我们需要在使用时手动指定注解的属性名称,使用value则无需填入:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    String test();
}
public class Main {
    @Test(test = "")
    public static void main(String[] args) {

    }
}

我们也可以使用default关键字来为这些属性指定默认值:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    String value() default "都看到这里了,给个三连吧!";
}

当属性存在默认值时,使用注解的时候可以不用传入属性值。当属性为数组时呢?

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    String[] value();
}

当属性为数组,我们在使用注解传参时,如果数组里面只有一个内容,我们可以直接传入一个值,而不是创建一个数组:

@Test("关注点了吗")
public static void main(String[] args) {
	
}
public class Main {
    @Test({"value1", "value2"})   //多个值时就使用花括号括起来
    public static void main(String[] args) {

    }
}

反射获取注解

既然我们的注解可以保留到运行时,那么我们来看看,如何获取我们编写的注解,我们需要用到反射机制:

public static void main(String[] args) {
    Class<Student> clazz = Student.class;
    for (Annotation annotation : clazz.getAnnotations()) {
        System.out.println(annotation.annotationType());   //获取类型
        System.out.println(annotation instanceof Test);   //直接判断是否为Test
        Test test = (Test) annotation;
        System.out.println(test.value());   //获取我们在注解中写入的内容
    }
}

通过反射机制,我们可以快速获取到我们标记的注解,同时还能获取到注解中填入的值,那么我们来看看,方法上的标记是不是也可以通过这种方式获取注解:

public static void main(String[] args) throws NoSuchMethodException {
    Class<Student> clazz = Student.class;
    for (Annotation annotation : clazz.getMethod("test").getAnnotations()) {
        System.out.println(annotation.annotationType());   //获取类型
        System.out.println(annotation instanceof Test);   //直接判断是否为Test
        Test test = (Test) annotation;
        System.out.println(test.value());   //获取我们在注解中写入的内容
    }
}

无论是方法、类、还是字段,都可以使用getAnnotations()方法(还有几个同名的)来快速获取我们标记的注解。

所以说呢,这玩意学来有啥用?丝毫get不到这玩意的用处。其实不是,现阶段你们还体会不到注解带来的快乐,在接触到Spring和SpringBoot等大型框架后,就能感受到注解带来的魅力了。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

几十款游戏的简单分析

文章目录 一、 介绍二、 影响游戏体验的因素三、 游戏能爆火的因素1.影响游戏爆火因素的排名2.玩游戏的两种经典心理3.经典案例分析Qq农场植物大战僵尸水果忍者召唤神龙羊了个羊 4.游戏公司可借鉴的经验5.未来游戏面对的诸多挑战 四、 几十款游戏的多方面分析FC红白游戏机十二人…

chatgpt赋能python:Python中的import使用详解

Python中的import使用详解 介绍 在Python中&#xff0c;import是将一个模块引入到当前脚本中使用的关键字。Python中的模块是指一个包含所有定义、函数和变量等的Python文件&#xff0c;也可以包含其他模块&#xff0c;从而构成一个Python程序。在Python中&#xff0c;有很多…

spark相关理论

系列文章目录 ubuntu虚拟机下搭建zookeeper集群&#xff0c;安装jdk压缩包&#xff0c;搭建Hadoop集群与spark集群的搭建【上篇】_ubuntu搭建zookeeper集群 ubuntu虚拟机下搭建zookeeper集群&#xff0c;安装jdk压缩包&#xff0c;搭建Hadoop集群与spark集群的搭建【下篇】 …

Redux基本使用和实践

Redux的核心是store&#xff0c;store作为应用状态的容器&#xff0c;保存着这个页面的状态数据树。 store 但是store本质上是一个JavaScript对象&#xff0c;这个对象含有了dispatch以及获取页面状态数据的方法等等。 如上图所示&#xff0c;store提供几个方法给开发者调用&…

[论文阅读笔记75]P-Tuning v2

1. 基本信息 题目论文作者与单位来源年份P-Tuning v2: Prompt Tuning Can Be Comparable to Fine-tuning Universally Across Scales and TasksXiao Liu等Tsinghua University清华大学2021 Citations, References 论文链接&#xff1a;https://arxiv.org/pdf/2110.07602.pdf…

chatgpt赋能python:Python的IDLE是什么?——初探IDLE的用途和功能

Python的IDLE是什么&#xff1f;——初探IDLE的用途和功能 Python的IDLE是一个Python集成开发环境(IDE)&#xff0c;可以简单地将其视为为开发者提供编写、调试和执行代码的工具。IDLE包括一个交互式解释器&#xff0c;使开发更加快速和简便。它还提供了代码编辑器、调试器和其…

chatgpt赋能python:Python技巧:一行代码实现所有数据的输出

Python技巧&#xff1a;一行代码实现所有数据的输出 Python是一种高级动态语言&#xff0c;因其简单易学和灵活性而广受欢迎。Python的语法简单明了&#xff0c;适合初学者学习、理解和实践&#xff0c;同时也是专业程序员的首选开发语言之一。 在实际的编程中&#xff0c;有…

k8s简单部署示例

1 部署yaml文件 1.1 Deployment部署 apiVersion: apps/v1 kind: Deployment metadata:name: zscorenamespace: wangzy-plabels:app: zscore-dep spec:replicas: 1selector:matchLabels:app: zscoretemplate:metadata:labels:app: zscoreannotations:sidecar.istio.io/inject:…

[工业互联-9]:EtherCAT(以太网控制自动化技术)+TwinCAT 在生产自动化控制中的应用 、

前言 EtherCAT&#xff08;以太网控制自动化技术&#xff09;是一个开放架构&#xff0c;以以太网为基础的现场总线系统&#xff0c;其名称的CAT为控制自动化技术&#xff08;Control Automation Technology&#xff09;字首的缩写。EtherCAT是确定性的工业以太网&#xff0c;…

Android 源码 AOSP版本– 下载[Ubuntu ]

Android 源码 AOSP版本– 下载[Ubuntu ] 前言配置下载源码前言 Android系统作为一个庞大的开源项目,除了一些谷歌自带服务之外,其他所有代码均以AOSP(Android Open Source Project)的形式开源。对于框架开发者来说,熟悉AOSP是必不可少的知识。即使是普通开发者,为了优化…

POSTGRES 多条件数量统计---CASE WHEN 妙用

创建表 create table tbl_user( id serial PRIMARY KEY, name varchar(256), addr varchar(256), age int, score int, fav varchar(256) ) 插入预置数据 INSERT INTO tbl_user (name, addr, age, score, fav) VALUES (aaa,aaa_addr,10, 23,aaa_fav_new), (bbb,ccc_addr,10, 23…

chatgpt赋能python:Python之妙用一行输出一个数字

Python之妙用一行输出一个数字 在日常编程中&#xff0c;我们常常需要输出一些数字来进行调试或测试。而在Python中&#xff0c;一行代码就可以轻松实现这个过程&#xff0c;即一行代码输出一个数字。 Python的print()函数 在Python中&#xff0c;print()函数是最基本的输出…

CPU lock-step资料整理

知识的价值在于分享&#xff0c;欢迎大家批评指正&#xff0c;共同进步。 目录 1 功能安全 2 技术特性 3 安全系统架构 4 TI Hercules系列 4.1 TMS570安全概念基本原理 4.1.1 1oo1D双核安全概念 4.1.2 1oo1D优势 总结 参考文献 1 功能安全 根据ISO26262-2018&#xff0…

踩坑集锦之Mybaits Invalid bound statement异常

踩坑集锦之Mybaits Invalid bound statement 引言多数据源场景下Mybaits如何进行配置包扫描过程问题一: 自动注入带来的同类型bean实例冲突问题二: 扫描器扫描路径重叠&#xff0c;导致优先级低的扫描器扫描不到对应包路径下的mapper接口补充说明1: MapperScannerConfigurer补充…

编译tolua——1、编译工具和环境说明

大家好&#xff0c;我是阿赵。 之前有朋友问我编译tolua相关的问题。虽然网上也有很多相关的资料可以查询&#xff0c;但我感觉自己也写一篇&#xff0c;作为一个记录也不错。不过一篇文章要把所有内容都说完&#xff0c;可能有点长&#xff0c;所以把整个过程分开几篇文章写一…

图文并茂五分钟搞懂react中的reducer

什么是 reducer 函数? 为什么要用 reducer? Reducer 是处理状态的另一种方式。通俗来讲&#xff0c;就是可以让你的复杂组件更加干净&#xff0c;代码更加优雅当你的组件里有好多个状态更新逻辑&#xff0c;并且有些是有一定关联性的&#xff0c;写多个useState会看起来很杂…

SPIFlash-W25QXX使用总结

W25QXX简介 W25QXX&#xff0c;后面的XX指的是Mbit 常见的型号有&#xff1a; W25Q80 W25Q16 W25Q32 W25Q64 W25Q128 注意80是表示8而不是80 所以&#xff0c;换算成字节数&#xff0c;从上到下为&#xff1a; 1MB 2MB 4MB 8MB 16MB 整个flash分成多个块&#xff0c;一个块分成…

Linux配置MySQL环境(三)

Linux配置MySQL环境 一、下载1. 官网下载MySQL2. 百度网盘快速下载MySQL 二、安装1、通过 Xftp 将 MySQL 安装包拷贝到 Linux2、解压缩3、安装 common、libs、client、server4、初步连接 三、卸载四、常用设置1. 修改 root 用户密码 五、使用新密码登录六、开启远程访问七、开放…

购物车按钮

先看效果&#xff1a; 再看代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>购物车按钮展示</title><link href"https://fonts.googleapis.com/css?familyInter:400…

002Mybatis初始化引入

引入依赖 <dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId> </dependency> 自动检测工程中的DataSource创建并注册SqlSessionFactory实例创建并注册SqlSessionTemplate实例自…