当前位置: 首页 > news >正文

初识Java 18-2 泛型

目录

构建复杂模型

类型擦除

C++中的泛型

迁移的兼容性

类型擦除存在的问题

边界的行为

对类型擦除的补偿

创建类型实例

泛型数组


本笔记参考自: 《On Java 中文版》


构建复杂模型

        泛型的一个优点就是,能够简单且安全地创建复杂模型

【例子:生成更复杂的数据结构】

import onjava.Tuple4;import java.util.ArrayList;public class TupleList<A, B, C, D>extends ArrayList<Tuple4<A, B, C, D>> {public static void main(String[] args) {TupleList<Vehicle, Amphibian, String, Integer> tl =new TupleList<>();tl.add(TupleTest2.h());tl.add(TupleTest2.h());tl.forEach(System.out::println);}
}

        程序执行的结果是:

        除此之外,我们也可以利用泛型组合各种各样的“块”,使其最终能够实现强大的功能。下面的例子表示的是一个商店(Store),这个商店中有通道(Aisle)、货架(Shelf)和商品(Product):

【例子:通过泛型构建一个商店模型】

import onjava.Suppliers;import java.util.ArrayList;
import java.util.Random;
import java.util.function.Supplier;// 构建商品模型
class Product {private final int id;private String description;private double price;Product(int idNumber, String descr, double price) {id = idNumber;description = descr;this.price = price;System.out.println(toString());}@Overridepublic String toString() {return id + ":" + description +", 价格:" + price + "元";}public void priceChange(double change) {price += change;}public static Supplier<Product> generator =new Supplier<Product>() {private Random rand = new Random(47);@Overridepublic Product get() {return new Product(rand.nextInt(1000),"某件商品", Math.round(rand.nextDouble() * 1000.0) + 0.99);}};
}// 构建货架模型
class Shelf extends ArrayList<Product> {Shelf(int nProducts) {// Suppliers需要由自己进行实现Suppliers.fill(this, Product.generator, nProducts);}
}// 构建通道模型
class Aisle extends ArrayList<Shelf> {Aisle(int nShelves, int nProducts) {for (int i = 0; i < nShelves; i++)add(new Shelf(nProducts));}
}class CheckOutStand {
}class Office {
}// 最终组合成了一个商店的模型
public class Store extends ArrayList<Aisle> {private ArrayList<CheckOutStand> checkouts =new ArrayList<>();private Office office = new Office();public Store(int nAisles, int nShelves, int nProducts) {for (int i = 0; i < nAisles; i++)add(new Aisle(nShelves, nProducts));}@Overridepublic String toString() {StringBuffer result = new StringBuffer();for (Aisle a : this)for (Shelf s : a)for (Product p : s) {result.append(p);result.append("\n");}return result.toString();}public static void main(String[] args) {System.out.println(new Store(3, 2, 2));}
}

        程序执行的结果是:

        从Store.toString()方法中可以看出:尽管经过了层层封装,但我们依旧可以方便、安全地管理这些模块。

        这里还需要注意自定义的Suppliers.fill()方法,这个方法的实现会在之后提到。此处的fill()方法可以等价于:

Stream.generate(Product.generator).limit(nProducts).forEach(this::add);

类型擦除

        Java的泛型同样存在着不合理之处。例如,尽管我们可以声明ArrayList.class,但却无法使用ArrayList<Integer>.class

【例子:发现泛型的不合理】

import java.util.ArrayList;public class ErasedTypeEquivalence {public static void main(String[] args) {Class c1 = new ArrayList<String>().getClass();Class c2 = new ArrayList<Integer>().getClass();System.out.println(c1 == c2);}
}

        程序执行,会返回true

        输出告诉我们,ArrayList<String>ArrayList<Integer>是相同的类型。但这是有问题的:因为它们的行为并不相同。我们无法将Integer对象放入到ArrayList<String>,却可以把Integer对象放入到ArrayList<Integer>中。

        除此之外,Java的泛型还有一个更麻烦的特性:

【例子:泛型代码内部的信息】

import java.util.*;class Frob {
}class Fnorkle {
}class Quark<Q> {
}class Particle<POSITION, MOMENTUM> {
}public class LostInformation {public static void main(String[] args) {List<Frob> list = new ArrayList<>();System.out.println(Arrays.toString(list.getClass().getTypeParameters()));Map<Frob, Fnorkle> map = new HashMap<>();System.out.println(Arrays.toString(map.getClass().getTypeParameters()));Quark<Fnorkle> quark = new Quark<>();System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));Particle<Long, Double> particle = new Particle<>();System.out.println(Arrays.toString(particle.getClass().getTypeParameters()));}
}

        程序执行的结果是:

        Class.getTypeParameters()方法会返回一个由类型变量的对象组成的数组,表示泛型对象声明(所声明)的类型变量。这似乎表示着我们可以获取与泛型参数有关的类型信息。但结果是,我们只能发现作为参数占位符的标识符。

        这就说明:在Java的泛型代码内部,并不存在有关泛型参数类型的可用信息

    而C++等语言是可以在泛型内部获取类型信息的。

        Java的泛型是通过类型擦除实现的。因此在使用泛型时,任何具体的类型信息都会被擦除。在泛型内部,唯一能够知道的事情就是我们在使用这个对象。因此,ArrayList<String>ArrayList<Integer>在运行时都被“擦除”成了它们的原始类型(raw type):ArrayList

(这种通过类型擦除实现的泛型有时也被称为第二类泛型类型)

C++中的泛型

        Java的设计中有许多参考了C++的元素。因此,二者在参数化类型的语法部分也十分相似:

【例子:C++中的泛型(即模板)】

#include <iostream>
using namespace std;template <class T>
class Manipulator {T obj;public:Manipulator(T x) {obj = x;}void manipulate() {obj.f();}
};class HasF {
public:void f() {cout << "HasF::f()" << endl;}
};int main() {HasF hf;Manipulator<HasF> manipulator(hf);manipulator.manipulate();
}

        编译并执行程序,可得:

        C++编译器会在实例化模板时进行检测。因此,在实例化Manipulator<HasF>时,编译器会发现HasF中存在着方法f()

        接下来再尝试通过Java实现同样的效果:

【例子:在Java中进行尝试】

        首先编写一个HasF类:

public class HasF {public void f(){System.out.println("HasF.f()");}
}

        但接下来的部分却没办法如C++一样书写。若我们尝试调用方法obj.f(),编译器就会提示我们:

因为类型擦除的缘故,编译器不会知道Manipulator<HasF>的类型参数是HasF,因此会认为这种调用是不安全的。若想要调用f(),我们就必须人为规定泛型类的边界,帮助编译器确定符合边界的类型:

public class Manipulator2<T extends HasF> {private T obj;Manipulator2(T x) {obj = x;}public void manipulator() {obj.f();}
}

        <T extends HasF>告诉编译器,T的类型必须HasF及其的子类

        在这里,泛型的类型参数被擦除为了其的第一个边界HasF(与之相对的,也存在拥有多重边界的泛型)。编译器会将类型参数替换为擦除后的类型,因此可以说,在这个例子中T被替换成了HasF

    并且,该例子实际上并不需要使用到泛型——可以直接使用更加具体的类型HasF

        注意:当我们希望代码能够跨越多个类型运行时,泛型才会发挥作用(因此,在具有实际价值的泛型代码中,类型参数及其应用往往会比简单的类替换更加复杂)。

(基于以上论点,可以认为<T extends HasF>这种用法存在缺陷。)

        下面的例子展示了更好的一种泛型应用:通过让方法返回类型参数T,可以使泛型返回精确的类型。

【例子:更好的泛型使用】

public class ReturnGenericType<T extends HasF> {private T obj;ReturnGenericType(T x) {obj = x;}public T get() {return obj;}
}

迁移的兼容性

        注意:类型擦除并不是一项语言特性。它是Java在实现泛型时使用的一种必要的折中,因为泛型并不是这门语言与生俱来的一部分。

    因此,Java中的泛型并没有将类型参数具体化成第一类实体的能力。

        因为类型擦除,泛型类型被视同第二类类型处理,这使得其无法在一些重要的上下文中得到使用:泛型类型只会在静态类型检查时存在,之后,程序会将泛型类型擦除成它们的非泛型上界

        在Java 5之前,存在许多编写完毕的非泛型的库。库是一门语言重要的组成部分,无法被轻易抛弃。因此,Java的泛型设计必然需要保证向后兼容性迁移兼容性。前者保证原有的数据依旧合法,后者则需要协调泛化的程序与非泛化的库(反之亦然)。

||| 至于类型擦除是否是一种好的手段,就只能靠时间来验证了。


类型擦除存在的问题

        类型擦除在非泛化代码和泛化代码之间构建起了一座桥梁,泛型得以在不破坏现有库的情况下加入Java。

        然而这种做法是有代价的。泛型代码无法用于需要显式引用运行时类型的操作,例如类型转换、instanceof操作以及new表达式。在编写泛型代码时,我们只是看起来掌握了参数的类型信息。就比如,现在有一个泛型类:

class Foo<T> {T var;
}

若为它创建一个实例:

Foo<Cat> f = new Foo<>();

尽管不论是直观的理解或是语法本身带来的暗示,都在说明T已经被替换成了Cat。遗憾的是,泛型内部的T已经只是一个Object

    泛型擦除像是一个边界,在边界里面的成员会被擦除成原始的类型。只有在进出边界时,它们才会被转换成对应的类型。

        另外,因为类型擦除和迁移兼容性,Java中泛型的使用并非是强制性的。

【例子:不强制的泛型使用】

class GenericBase<T> {private T element;public void set(T arg) {element = arg;}public T get() {return element;}
}// 使用泛型:
class Derived1<T> extends GenericBase<T> {}// 使用原始类型,但未发出警告:
class Derived2 extends GenericBase {}// 引发错误:
//class Derived3 extends GenericBase<?> {}public class ErasureAndInheritance {@SuppressWarnings("unchecked")public static void main(String[] args) {Derived2 d2 = new Derived2();Object obj = d2.get();d2.set(obj); // d2.set()会引发警告,使用@SuppressWarnings()进行关闭}
}

        Derived2继承了GenericBase,而未使用泛型参数。编译器没有在这里给出警告,直到进行编译时,在d2.get()才显现出来。若要关闭警告,可以使用Java提供的注解:

@SuppressWarnings("unchecked")

这一注解应该被放置于触发警告的类上。

        Derived3会引发错误:

编译器需要的是一个原始的基类,而我们却提供了一个带有<?>的泛型。

    在Java中,使用类型参数就意味着我们需要管理边界。这使得Java泛型并没有完全发挥其应有的灵活性。


边界的行为

        类型擦除使得泛型会表现出一些无意义的行为:

【例子:无意义的泛型行为】

import java.lang.reflect.Array;
import java.util.Arrays;public class ArrayMaker<T> {private Class<T> kind;public ArrayMaker(Class<T> kind) {this.kind = kind;}@SuppressWarnings("unchecked")T[] create(int size) { // 需要使用类型转换return (T[]) Array.newInstance(kind, size);}public static void main(String[] args) {ArrayMaker<String> stringMaker =new ArrayMaker<>(String.class);String[] stringArray = stringMaker.create(9);System.out.println(Arrays.toString(stringArray));}
}

        程序执行的结果是:

        在这个例子中,尽管kind看起来会获得一个具体的Class<T>,但当进入类型擦除的边界时,<T>就会消失。换言之,kind中存储的只是一个无意义的Class,它无法生成一个具体的结果,因此我们还需要使用到类型转换(并且会产生警告)。

    在create()方法中使用到的Array.newInstance(),是在泛型中创建数组的推荐方法。

        我们也可以使用泛型创建集合(而不是数组):

【例子:无意义的集合】

import java.util.ArrayList;
import java.util.List;public class ListMaker<T> {List<T> create() {return new ArrayList<>();}public static void main(String[] args) {ListMaker<String> stringMaker = new ListMaker<>();List<String> stringList = stringMaker.create();}
}

        在create()内部的new ArrayList<>()方法中没有使用<T>,并且在运行时<T>也会被移除。这使得这个集合似乎并没有具体意义,但若使用new ArrayList(),依旧会引发警告。

---

        但我们依旧可以通过一些方式进行有意义的调用:

【例子:有意义的泛型集合】

import onjava.Suppliers;import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;public class FilledList<T> extends ArrayList<T> {FilledList(Supplier<T> gen, int size) {// 等价于使用Stream.generate(gen)生成size个元素,并装入该类中Suppliers.fill(this, gen, size);}public FilledList(T t, int size) {for (int i = 0; i < size; i++)this.add(t);}public static void main(String[] args) {List<String> list = new FilledList<>("Hello", 4);System.out.println(list);// 也可以借由Supplier接口进行实现List<Integer> ilist = new FilledList<>(() -> 47, 4);System.out.println(ilist);}
}

        程序执行的结果是:

        虽然在this.add()方法中,编译器无法知道任何关于T的信息,但我们依旧可以在编译时确保放入FilledList中的是类型T。因此,尽管存在类型擦除,编译器依旧可以确保类型在使用方法内部的一致性。

        现在,泛型运行时的关键就指向的边界——对象进入和离开方法体的临界点。编译器在这里执行类型检查,插入类型转换。可以观察下面两个类之间的区别:

        这两个类之间唯一的区别就是它们是否使用了泛型。现在可以通过反编译指令(javap -c)来观察它们的字节码:

        可以发现,非泛型类和泛型类在这里得到的字节码完全相同。在main()中调用set()方法时,编译器自动插入了类型转换,并且get()的类型转换仍然存在。从这里可以得出一个结论:泛型所有的行为都发生在边界,包括输入值的检查、类型转换等。

对类型擦除的补偿

        类型擦除会使得我们在一些操作上受掣肘:

        尽管我们有时可以绕过这些问题,但有些问题总是需要泛型来解决。此时可以使用类型标签来补偿类型擦除带来的损失:我们可以在类型表达式中显示地为所使用的类型传入一个Class对象

        类型标签与instanceof的不同之于,instanceof是静态的检查,若类型被擦除,那么instanceof就会失效。而类型标签可以通过isInstance()提供动态的检查,这使得它可以在泛型中进行使用:

【例子:使用类型标签】

class Building {
}class House extends Building {
}public class ClassTypeCapture<T> {Class<T> kind;public ClassTypeCapture(Class<T> kind) {this.kind = kind;}public boolean f(Object arg) {return kind.isInstance(arg);}public static void main(String[] args) {ClassTypeCapture<Building> ctt1 =new ClassTypeCapture<>(Building.class);System.out.println(ctt1.f(new Building()));System.out.println(ctt1.f(new House()));ClassTypeCapture<House> ctt2 =new ClassTypeCapture<>(House.class);System.out.println(ctt2.f(new Building()));System.out.println(ctt2.f(new House()));}
}

        程序执行的结果是:

        编译器会确保类型标签能够与泛型参数相匹配。

创建类型实例

        在Erased.java中执行new T()操作是无法成功的,这有两个原因:①类型擦除和②编译器无法验证T中是否存在无参构造器。但C++却支持这种操作:

【例子:C++允许创建泛型的类型实例】

template<class T> class Foo {T x; // 字段xT* y; // 指向T类的指针
public:Foo() {// 初始化指针y = new T();}
};class Bar {};int main()
{Foo<Bar> fb;Foo<int> fi; // 甚至可以使用基本类型return 0;
}

---

        而Java则需要使用工厂设计方法来创建新的实例。Class就是一个方便的工厂对象,将其作为类型标签,我们能够在Java中实现类似上例的功能:

【例子:通过newInstance()创建泛型对象】

import java.util.function.Supplier;class ClassAsFactory<T> implements Supplier<T> {Class<T> kind;ClassAsFactory(Class<T> kind) {this.kind = kind;}@Overridepublic T get() {try {return kind.getConstructor().newInstance();} catch (Exception e) {throw new RuntimeException(e);}}
}class Employee {public Employee() {}@Overridepublic String toString() {return "Employee";}
}public class InstantiateGenericType {public static void main(String[] args) {ClassAsFactory<Employee> fe =new ClassAsFactory<>(Employee.class);System.out.println(fe.get());ClassAsFactory<Integer> fi =new ClassAsFactory<>(Integer.class);try {System.out.println(fi.get());} catch (Exception e) {System.out.println(e.getMessage());}}
}

        程序执行的结果是:

        在该例中,我们尝试创建Integer的实例,结果却失败了。这是因为Integer不存在无参构造器。这个错误不会在编译时被发现,也因此上例的方式并不被推荐。更好的方式是使用显式工厂,同时限制能够传入的类型:

【例子:创建工厂,生成泛型实例】

import onjava.Suppliers;import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;class IntegerFactory implements Supplier<Integer> {private int i = 0;@Overridepublic Integer get() {return ++i;}
}class Widget {private int id;Widget(int n) {id = n;}@Overridepublic String toString() {return "Widget " + id;}public staticclass Factory implements Supplier<Widget> {private int i = 0;@Overridepublic Widget get() {return new Widget(++i);}}
}class Fudge {private static int count = 1;private int n = count++;@Overridepublic String toString() {return "Fudge " + n;}
}class Foo2<T> {private List<T> x = new ArrayList<>();Foo2(Supplier<T> factory) {// 等价于使用Stream.generate(factory)生成5个元素,并装入该类中Suppliers.fill(x, factory, 5);}@Overridepublic String toString() {return x.toString();}
}public class FactoryConstraint {public static void main(String[] args) {System.out.println(new Foo2<>(new IntegerFactory()));System.out.println(new Foo2<>(new Widget.Factory()));System.out.println(new Foo2<>(Fudge::new));}
}

        程序执行的结果是:

        Foo2类用于调用各种工厂方法,生成实例。这里展示了三种创建工厂的方式:

  1. IntegerFactory:本身就是一个实现了Supplier<Integer>的工厂;
  2. Widget:包含了一个作为工厂的内部类;
  3. Fudge:不执行任何类似工厂的操作,但编译器会自动将Fudge::new转变成对get的调用。

        除此之外,还有另一种设计模式:模板方法。将方法在子类中进行重写,用来生成对应类型的对象:

【例子:使用模板方法生成泛型实例】

abstract class GenericWithCreate<T> {final T element;GenericWithCreate() {element = create();}// 会在子类中重写的模板方法:abstract T create();
}class X {
}class XCreator extends GenericWithCreate<X> {@OverrideX create() {return new X();}void f() {System.out.println(element.getClass().getSimpleName());}
}public class CreatorGeneric {public static void main(String[] args) {XCreator xc = new XCreator();xc.f();}
}

        程序执行的结果是:

        GenericWithCreate有唯一的无参构造器,这样就可以要求任何所有这个类的程序员,必须通过我们规定的方式初始化这个类。另一边,create()方法将类的创建逻辑交付给了子类实现,这使得该方法的返回值可以在子类中得到更具体的定义。


泛型数组

        正如之前所看到的,我们无法直接在泛型中创建泛型数组:

一个直接的方法是使用集合来代替数组:

【例子:使用集合替代数组】

import java.util.ArrayList;
import java.util.List;public class ListOfGenerics<T> {private List<T> array = new ArrayList<>();public void add(T item) {array.add(item);}public T get(int index) {return array.get(index);}
}

        这样我们就获得了数组的行为,并且得到了泛型提供的编译时类型检查。

        但如果确实有使用泛型数组的必要,那么可以尝试使用一个泛型引用,通过将这个引用指向一个数组,可以变相满足编译器的规定:

【例子:将引用指向数组】

class Generic<T> {
}public class ArrayOfGenericReference {static Generic<Integer>[] gia;
}

        因为类型擦除,这个数组实际上没有具体的类型,无论指定的泛型参数是什么,数组都会具有相同的结构和大小。这看上去有点像Object,那么我们是否可以将一个Object类型的数组转换成目标数组?

        答案依旧是否定的:

【例子:无法对Object数组进行转型】

public class ArrayOfGeneric {static final int SIZE = 100;static Generic<Integer>[] gia;@SuppressWarnings("unchecked")public static void main(String[] args) {try {gia = (Generic<Integer>[]) new Object[SIZE];} catch (ClassCastException e) {System.out.println(e.getMessage());}// 运行时会发生类型擦除,得到的是原始类型Generic[]gia = (Generic<Integer>[]) new Generic[SIZE];System.out.println(gia.getClass().getSimpleName());gia[0] = new Generic<>();// 发生编译时错误(类型不匹配):// gia[1] = new Object();// gia[2] = new Generic<Double>();}
}

        程序执行的结果是(输出已经过折叠)

        数组的类型在它们被创建的时候才会确定下来,因此转型信息Generic<Integer>[]也只会存在于编译时。语句:

gia = (Generic<Integer>[]) new Object[SIZE];

能得到的只会是Object数组,这就会导致问题。

        而另一条创建语句:

gia = (Generic<Integer>[]) new Generic[SIZE];

对一个被擦除类型的数组进行强制类型转换得到了成功。这也是唯一可以成功创建泛型数组的方式

        以此类推,下面是一个更加复杂的例子:

【例子:更复杂的泛型数组尝试】

public class GenericArray<T> {private T[] array;@SuppressWarnings("unchecked")public GenericArray(int sz) {array = (T[]) new Object[sz];}public void put(int index, T item) {array[index] = item;}public T get(int index) {return array[index];}// 通过返回T[],可以发现其的潜在表现形式:public T[] rep() {return array;}public static void main(String[] args) {GenericArray<Integer> gai = new GenericArray<>(10);try {Integer[] ia = gai.rep();} catch (ClassCastException e) {System.out.println(e.getMessage());}// 可以使用Object数组接受:Object[] oa = gai.rep();}
}

        程序执行的结果是(输出已经过折叠)

        显然,这里的gai也被类型擦除影响,其在运行时的实际类型也变成了Object

        之前也提到过,通过@SuppressWarnings("unchecked")可以抑制编译器发出警告,否则会出现这样的警告:

        为了获取更加详细的信息,可以在编译时添加-Xlint:unchecked选项。而如果这么做,就会得到如下的信息:

    若认为报出的警告并不影响程序运行,就可以使用注解关闭警告,因为警告在一些时候也会成为不必要的噪声。

        因为类型擦除,在上例中我们只能得到Object[]。若此时立刻将其转变为T[],就会丢失数组的实际类型,这可能会让一些潜在错误有机可乘。

        一个可能的替代方法是在泛型类内部使用Object数组,而在边界处执行类型转换:

【例子:在边界上执行类型转换】

public class GenericArray2<T> {private Object[] array;public GenericArray2(int sz) {array = new Object[sz];}public void put(int index, T item) {array[index] = item;}@SuppressWarnings("unchecked")public T get(int index) {return (T) array[index];}// 该方法依旧存在问题:为检测的类型转换@SuppressWarnings("unchecked")public T[] rep() {return (T[]) array;}public static void main(String[] args) {GenericArray2<Integer> gai =new GenericArray2<>(10);for (int i = 0; i < 10; i++)gai.put(i, i);for (int i = 0; i < 10; i++)System.out.print(gai.get(i) + " ");System.out.println();try {Integer[] ia = gai.rep();} catch (Exception e) {System.out.println(e);}}
}

        程序执行的结果是:

        这么做依旧需要抑制警告。但比上一个例子更好的一点在于,现在get()方法能够正确地进行类型转换了。而不好的一点在于,rep()方法依旧无法将Object[]转型为T[]。这里就可以得出一个结论:底层的数组类型是无法更改的,这个类型只能是Object[]

    在泛型类内部使用Object[]的另一个好处是,让程序员花费更少的精力来处理数组的运行时类型。

---

        既然底层的数组无法更改,那么我们还可以换一个思路。通过类型标记,我们可以直接创建一个目标数组的实例:

【例子:使用类型标记创建数组实例】

import java.lang.reflect.Array;public class GenericArrayWithTypeToken<T> {private T[] array;@SuppressWarnings("unchecked")public GenericArrayWithTypeToken(Class<T> type, int sz) {array = (T[]) Array.newInstance(type, sz);}public void put(int index, T item) {array[index] = item;}public T get(int index) {return array[index];}// 依旧会暴露潜在的表达方式:public T[] rep() {return array;}public static void main(String[] args) {GenericArrayWithTypeToken<Integer> gai =new GenericArrayWithTypeToken<>(Integer.class, 10);// 现在可以正常运行:Integer[] ia = gai.rep();}
}

        尽管还是需要抑制警告,但在这个例子中,数组在运行时是精确的T[]类型了。

    然而,在Java的源代码中,也存在着许都使用Object数组转型为参数化类型的操作,对其编译甚至会产生警告……(因此,Java的库代码难以作为我们自己编写代码时的范例)

相关文章:

初识Java 18-2 泛型

目录 构建复杂模型 类型擦除 C中的泛型 迁移的兼容性 类型擦除存在的问题 边界的行为 对类型擦除的补偿 创建类型实例 泛型数组 本笔记参考自&#xff1a; 《On Java 中文版》 构建复杂模型 泛型的一个优点就是&#xff0c;能够简单且安全地创建复杂模型。 【例子&am…...

vue分环境打包及案例代码

Vue分环境打包可以帮助我们针对不同的环境(如开发环境、测试环境、生产环境等)打包出不同的版本,以满足不同的需求。下面是一个简单的Vue分环境打包的示例代码: 安装cross-env: npm install --save-dev cross-env在项目的根目录下创建不同的环境配置文件,如test.env.js…...

基于springboot+vue的在线考试系统(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…...

重装linux后需要做的配置

1. linux中 vim如果输入中文乱码 打开/etc/vim/vimrc输入&#xff1a; set fileencodingsutf-8,gbk set termencodingutf-8 set encodingutf-8 把vim的缩进格式顺便改了 http://t.csdnimg.cn/K3ncc 2. 配置sudo授权用户 3. 新导入项目后 , chmod -R x 添加权限 4. 查询主机i…...

【华为数通HCIP | 网络工程师】821刷题日记-IS-IS(2)

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大三在校生&#xff0c;喜欢AI编程&#x1f38b; &#x1f43b;‍❄️个人主页&#x1f947;&#xff1a;落798. &#x1f43c;个人WeChat&#xff1a;hmmwx53 &#x1f54a;️系列专栏&#xff1a;&#x1f5bc;️…...

Linux系统-----进程管理(进程的创建与控制)

目录 前言 进程 1.基本概念 2.特征 3.Linux系统的进程 进程的创建 1. fork()函数 2. 多进程的创建与输出 进程的控制 1. exec()系列 2. wait() 函数 3. execl( )和fork( )联合使用 4. exit&#xff08; &#xff09; 前言 前面我们学习了Linux系统的基本指令以及如…...

Unity 获取物体的子物体的方法

Unity 中要获取物体的子物体&#xff0c;可以使用以下一些方法。 1、只获取一级节点的子物体&#xff1a; public Transform tran;// Start is called before the first frame updatevoid Start(){foreach (Transform child in tran){Debug.Log(child.name);}} 使用该方法只会…...

RocketMQ 读写压测

一、Producer 压测 [rootsz-glbd-mq-108-249 rocketmq-all-5.1.3-bin-release]# sh bin/tools.sh org.apache.rocketmq.example.benchmark.Producer -n 10.XXX.108.249:9876 -t TopicTest_LEXIN_2023_pop_128 -w 64 16:39:18,402 |-INFO in org.apache.rocketmq.logging.ch.qo…...

PHP调用API接口的方法及实现(一键采集淘宝商品详情数据)

随着互联网、云计算和大数据时代的到来&#xff0c;越来越多的应用程序需要调用第三方的API接口来获取数据&#xff0c;实现数据互通和协同工作。PHP作为一种常用的服务器端语言&#xff0c;也可以通过调用API接口来实现不同系统的数据交互和整合。本文将介绍PHP调用API接口的方…...

得物App安卓冷启动优化-Application篇

前言 冷启动指标是App体验中相当重要的指标&#xff0c;在电商App中更是对用户的留存意愿有着举足轻重的影响。通常是指App进程启动到首页首帧出现的耗时&#xff0c;但是在用户体验的角度来看&#xff0c;应当是从用户点击App图标&#xff0c;到首页内容完全展示结束。 将启…...

【实战教程】PHP与七牛云的完美对接

前言&#xff1a; 随着互联网的迅速发展&#xff0c;越来越多的网站和应用程序需要处理大量的图片、视频和其他文件。为了有效地存储和管理这些文件&#xff0c;并提供快速的内容分发服务&#xff0c;开发者们常常依赖于云存储和CDN服务提供商。 七牛云是一家领先的云存储和C…...

Go 接口:nil接口为什么不等于nil?

一、Go 接口的地位 Go 语言核心团队的技术负责人 Russ Cox 也曾说过这样一句话&#xff1a;“如果要从 Go 语言中挑选出一个特性放入其他语言&#xff0c;我会选择接口”&#xff0c;这句话足以说明接口这一语法特性在这位 Go 语言大神心目中的地位。 为什么接口在 Go 中有这么…...

(UM1724) STM32 Nucleo-64 boards User manual

STM32 Nucleo-64 评估板用户手册 0. 前言1. 介绍[^1]2. Ordering information2.1 包装编号说明3. 开发环境4. 跳线端子 ON/OFF 的约定5. 快速入门6. 硬件布局与配置6.10 扩展连接器6.11 ARDUINO 连接器6.12 ST morpho 连接器7. Nucleo-64 评估板信息0. 前言 【相关博文】 【STM…...

SourceInsight - Relation Windows

磨刀不误砍柴工&#xff0c;你使用的工具决定了你的下限。我平时使用较多的代码编辑工具就是SourceInsight&#xff0c;这个工具速度快&#xff0c;操作方便&#xff0c;但处理非常大的项目的性能不是很理想&#xff0c;比如你要是添加整个Linux Kernel的源代码的话。 在使用SI…...

二维数组处理

输入整型二维数组a&#xff08;5行5列&#xff09;&#xff0c;完成如下要求&#xff1a; 输出二维数组a。 将a的第2行和第4行元素对调后&#xff0c;形成新的二维数组a并按行输出&#xff0c;每个元素之间隔一个空格。(行号从0开始计算)。 用对角线&#xff08;指二维数组左…...

线性表——(2)线性表的顺序存储及其运算的实现

归纳编程学习的感悟&#xff0c; 记录奋斗路上的点滴&#xff0c; 希望能帮到一样刻苦的你&#xff01; 如有不足欢迎指正&#xff01; 共同学习交流&#xff01; &#x1f30e;欢迎各位→点赞 &#x1f44d; 收藏⭐ 留言​&#x1f4dd; 看到美好&#xff0c;感受美好&a…...

数据结构 -- 图论之最小生成树

目录 1.最小生成树算法 1.Kruskal算法 2.Prim算法 1.最小生成树算法 定义:最小生成树算法:连通图有n个顶点组成,那么此时的图的每一个点都能相互连接并且边的个数为n-1条,那么此时该图就是最小生成树. 下面量算法有几个共同的特点: 1.只能使用图中权值最小的边来构造生成树 …...

【已解决】游戏缺少xinput1_3.dll的详细解决方案与详情解析

在现代科技日新月异的时代&#xff0c;电脑已经成为我们生活和工作中不可或缺的工具。然而&#xff0c;由于各种原因&#xff0c;电脑可能会出现一些问题&#xff0c;其中之一就是xinput1_3.dll文件的缺失。本文将详细介绍xinput1_3.dll丢失对电脑的影响以及丢失的原因&#xf…...

华天动力-OA8000 MyHttpServlet 文件上传漏洞复现

0x01 产品简介 华天动力OA是一款将先进的管理思想、 管理模式和软件技术、网络技术相结合&#xff0c;为用户提供了低成本、 高效能的协同办公和管理平台。 0x02 漏洞概述 华天动力OA MyHttpServlet 存在任意文件上传漏洞&#xff0c;未经身份认证的攻击者可上传恶意的raq文件…...

小航助学题库蓝桥杯题库c++选拔赛(23年8月)(含题库教师学生账号)

需要在线模拟训练的题库账号请点击 小航助学编程在线模拟试卷系统&#xff08;含题库答题软件账号&#xff09; 需要在线模拟训练的题库账号请点击 小航助学编程在线模拟试卷系统&#xff08;含题库答题软件账号&#xff09;...

论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(二)

HoST框架核心实现方法详解 - 论文深度解读(第二部分) 《Learning Humanoid Standing-up Control across Diverse Postures》 系列文章: 论文深度解读 + 算法与代码分析(二) 作者机构: 上海AI Lab, 上海交通大学, 香港大学, 浙江大学, 香港中文大学 论文主题: 人形机器人…...

Java 语言特性(面试系列1)

一、面向对象编程 1. 封装&#xff08;Encapsulation&#xff09; 定义&#xff1a;将数据&#xff08;属性&#xff09;和操作数据的方法绑定在一起&#xff0c;通过访问控制符&#xff08;private、protected、public&#xff09;隐藏内部实现细节。示例&#xff1a; public …...

《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》

引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...

基于服务器使用 apt 安装、配置 Nginx

&#x1f9fe; 一、查看可安装的 Nginx 版本 首先&#xff0c;你可以运行以下命令查看可用版本&#xff1a; apt-cache madison nginx-core输出示例&#xff1a; nginx-core | 1.18.0-6ubuntu14.6 | http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages ng…...

Go 语言接口详解

Go 语言接口详解 核心概念 接口定义 在 Go 语言中&#xff0c;接口是一种抽象类型&#xff0c;它定义了一组方法的集合&#xff1a; // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的&#xff1a; // 矩形结构体…...

微信小程序 - 手机震动

一、界面 <button type"primary" bindtap"shortVibrate">短震动</button> <button type"primary" bindtap"longVibrate">长震动</button> 二、js逻辑代码 注&#xff1a;文档 https://developers.weixin.qq…...

Nuxt.js 中的路由配置详解

Nuxt.js 通过其内置的路由系统简化了应用的路由配置&#xff0c;使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...

【单片机期末】单片机系统设计

主要内容&#xff1a;系统状态机&#xff0c;系统时基&#xff0c;系统需求分析&#xff0c;系统构建&#xff0c;系统状态流图 一、题目要求 二、绘制系统状态流图 题目&#xff1a;根据上述描述绘制系统状态流图&#xff0c;注明状态转移条件及方向。 三、利用定时器产生时…...

华为云Flexus+DeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建

华为云FlexusDeepSeek征文&#xff5c;DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建 前言 如今大模型其性能出色&#xff0c;华为云 ModelArts Studio_MaaS大模型即服务平台华为云内置了大模型&#xff0c;能助力我们轻松驾驭 DeepSeek-V3/R1&#xff0c;本文中将分享如何…...

Android Bitmap治理全解析:从加载优化到泄漏防控的全生命周期管理

引言 Bitmap&#xff08;位图&#xff09;是Android应用内存占用的“头号杀手”。一张1080P&#xff08;1920x1080&#xff09;的图片以ARGB_8888格式加载时&#xff0c;内存占用高达8MB&#xff08;192010804字节&#xff09;。据统计&#xff0c;超过60%的应用OOM崩溃与Bitm…...