二十、泛型(3)

news2025/1/13 15:34:02

本章概要

  • 构建复杂模型
  • 泛型擦除
    • C++ 的方式
    • 迁移兼容性
    • 擦除的问题
    • 边界处的动作

构建复杂模型

泛型的一个重要好处是能够简单安全地创建复杂模型。例如,我们可以轻松地创建一个元组列表:

TupleList.java

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);
    }
}

在这里插入图片描述

相关类:

Amphibian.java

public class Amphibian {
}

Tuple.java

public class Tuple {
    public static <A, B> Tuple2<A, B> tuple(A a, B b) {
        return new Tuple2<>(a, b);
    }

    public static <A, B, C> Tuple3<A, B, C>
    tuple(A a, B b, C c) {
        return new Tuple3<>(a, b, c);
    }

    public static <A, B, C, D> Tuple4<A, B, C, D>
    tuple(A a, B b, C c, D d) {
        return new Tuple4<>(a, b, c, d);
    }

    public static <A, B, C, D, E>
    Tuple5<A, B, C, D, E> tuple(A a, B b, C c, D d, E e) {
        return new Tuple5<>(a, b, c, d, e);
    }
}

Tuple2.java

public class Tuple2<A, B> {
    public final A a1;
    public final B a2;

    public Tuple2(A a, B b) {
        a1 = a;
        a2 = b;
    }

    public String rep() {
        return a1 + ", " + a2;
    }

    @Override
    public String toString() {
        return "(" + rep() + ")";
    }
}

Tuple3.java

public class Tuple3<A, B, C> extends Tuple2<A, B> {
    public final C a3;

    public Tuple3(A a, B b, C c) {
        super(a, b);
        a3 = c;
    }

    @Override
    public String rep() {
        return super.rep() + ", " + a3;
    }
}

Tuple4.java

public class Tuple4<A, B, C, D>
        extends Tuple3<A, B, C> {
    public final D a4;

    public Tuple4(A a, B b, C c, D d) {
        super(a, b, c);
        a4 = d;
    }

    @Override
    public String rep() {
        return super.rep() + ", " + a4;
    }
}

Tuple5.java

public class Tuple5<A, B, C, D, E>
        extends Tuple4<A, B, C, D> {
    public final E a5;

    public Tuple5(A a, B b, C c, D d, E e) {
        super(a, b, c, d);
        a5 = e;
    }

    @Override
    public String rep() {
        return super.rep() + ", " + a5;
    }
}

TupleTest2.java

public class TupleTest2 {
    static Tuple2<String, Integer> f() {
        return tuple("hi", 47);
    }

    static Tuple2 f2() {
        return tuple("hi", 47);
    }

    static Tuple3<Amphibian, String, Integer> g() {
        return tuple(new Amphibian(), "hi", 47);
    }

    static Tuple4<Vehicle, Amphibian, String, Integer> h() {
        return tuple(
                new Vehicle(), new Amphibian(), "hi", 47);
    }

    static Tuple5<Vehicle, Amphibian,
            String, Integer, Double> k() {
        return tuple(new Vehicle(), new Amphibian(),
                "hi", 47, 11.1);
    }

    public static void main(String[] args) {
        Tuple2<String, Integer> ttsi = f();
        System.out.println(ttsi);
        System.out.println(f2());
        System.out.println(g());
        System.out.println(h());
        System.out.println(k());
    }
}

这将产生一个功能强大的数据结构,而无需太多代码。

下面是第二个例子。每个类都是组成块,总体包含很多个块。在这里,该模型是一个具有过道,货架和产品的零售商店:

Suppliers.java

import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class Suppliers {
    // Create a collection and fill it:
    public static <T, C extends Collection<T>> C
    create(Supplier<C> factory, Supplier<T> gen, int n) {
        return Stream.generate(gen)
                .limit(n)
                .collect(factory, C::add, C::addAll);
    }

    // Fill an existing collection:
    public static <T, C extends Collection<T>> C fill(C coll, Supplier<T> gen, int n) {
        Stream.generate(gen)
                .limit(n)
                .forEach(coll::add);
        return coll;
    }

    // Use an unbound method reference to
    // produce a more general method:
    public static <H, A> H fill(H holder,
                                BiConsumer<H, A> adder, Supplier<A> gen, int n) {
        Stream.generate(gen)
                .limit(n)
                .forEach(a -> adder.accept(holder, a));
        return holder;
    }
}

Store.java

import java.util.*;
import java.util.function.*;

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());
    }

    @Override
    public String toString() {
        return id + ": " + description +
                ", price: $" + price;
    }

    public void priceChange(double change) {
        price += change;
    }

    public static Supplier<Product> generator =
            new Supplier<Product>() {
                private Random rand = new Random(47);

                @Override
                public Product get() {
                    return new Product(rand.nextInt(1000), "Test",
                            Math.round(
                                    rand.nextDouble() * 1000.0) + 0.99);
                }
            };
}

class Shelf extends ArrayList<Product> {
    Shelf(int nProducts) {
        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));
        }
    }

    @Override
    public String toString() {
        StringBuilder result = new StringBuilder();
        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(5, 4, 3));
    }
}

在这里插入图片描述

Store.toString() 显示了结果:尽管有复杂的层次结构,但多层的集合仍然是类型安全的和可管理的。令人印象深刻的是,组装这样的模型并不需要耗费过多精力。

Shelf 使用 Suppliers.fill() 这个实用程序,该实用程序接受 Collection (第一个参数),并使用 Supplier (第二个参数),以元素的数量为 n (第三个参数)来填充它。 Suppliers 类将会在本章末尾定义,其中的方法都是在执行某种填充操作,并在本章的其他示例中使用。

泛型擦除

当你开始更深入地钻研泛型时,会发现有大量的东西初看起来是没有意义的。例如,尽管可以说 ArrayList.class,但不能说成 ArrayList<Integer>.class。考虑下面的情况:

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);
    }

}

ArrayList<String>ArrayList<Integer> 应该是不同的类型。不同的类型会有不同的行为。例如,如果尝试向 ArrayList<String> 中放入一个 Integer,所得到的行为(失败)和向 ArrayList<Integer> 中放入一个 Integer 所得到的行为(成功)完全不同。然而上面的程序认为它们是相同的类型。

下面的例子是对该谜题的补充:

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<>();
        Map<Frob, Fnorkle> map = new HashMap<>();
        Quark<Fnorkle> quark = new Quark<>();
        Particle<Long, Double> p = new Particle<>();
        System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(p.getClass().getTypeParameters()));
    }

}

在这里插入图片描述

根据 JDK 文档,Class.getTypeParameters() “返回一个 TypeVariable 对象数组,表示泛型声明中声明的类型参数…” 这暗示你可以发现这些参数类型。但是正如上例中输出所示,你只能看到用作参数占位符的标识符,这并非有用的信息。

残酷的现实是:

在泛型代码内部,无法获取任何有关泛型参数类型的信息。

因此,你可以知道如类型参数标识符和泛型边界这些信息,但无法得知实际的类型参数从而用来创建特定的实例。如果你曾是 C++ 程序员,那么这个事实会让你很沮丧,在使用 Java 泛型工作时,它是必须处理的最基本的问题。

Java 泛型是使用擦除实现的。这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此,List<String>List<Integer> 在运行时实际上是相同的类型。它们都被擦除成原生类型 List

理解擦除并知道如何处理它,是你在学习 Java 泛型时面临的最大障碍之一。这也是本节将要探讨的内容。

C++ 的方式

下面是使用模版的 C++ 示例。你会看到类型参数的语法十分相似,因为 Java 是受 C++ 启发的:

// generics/Templates.cpp

#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();
}
/* Output:
HasF::f()
*/

Manipulator 类存储了一个 T 类型的对象。manipulate() 方法会调用 obj 上的 f() 方法。它是如何知道类型参数 T 中存在 f() 方法的呢?C++ 编译器会在你实例化模版时进行检查,所以在 Manipulator<HasF> 实例化的那一刻,它看到 HasF 中含有一个方法 f()。如果情况并非如此,你就会得到一个编译期错误,保持类型安全。

用 C++ 编写这种代码很简单,因为当模版被实例化时,模版代码就知道模版参数的类型。Java 泛型就不同了。下面是 HasF 的 Java 版本:

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

如果我们将示例的其余代码用 Java 实现,就不会通过编译:

class Manipulator<T> {
    private T obj;

    Manipulator(T x) {
        obj = x;
    }

    // Error: cannot find symbol: method f():
    public void manipulate() {
        obj.f();
    }
}

public class Manipulation {
    public static void main(String[] args) {
        HasF hf = new HasF();
        Manipulator<HasF> manipulator = new Manipulator<>(hf);
        manipulator.manipulate();
    }
}

因为擦除,Java 编译器无法将 manipulate() 方法必须能调用 objf() 方法这一需求映射到 HasF 具有 f() 方法这个事实上。为了调用 f(),我们必须协助泛型类,给定泛型类一个边界,以此告诉编译器只能接受遵循这个边界的类型。这里重用了 extends 关键字。由于有了边界,下面的代码就能通过编译:

public class Manipulator2<T extends HasF> {
    private T obj;

    Manipulator2(T x) {
        obj = x;
    }

    public void manipulate() {
        obj.f();
    }
}

边界 <T extends HasF> 声明 T 必须是 HasF 类型或其子类。如果情况确实如此,就可以安全地在 obj 上调用 f() 方法。

我们说泛型类型参数会擦除到它的第一个边界(可能有多个边界,稍后你将看到)。我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例,T 擦除到了 HasF,就像在类的声明中用 HasF 替换了 T 一样。

你可能正确地观察到了泛型在 Manipulator2.java 中没有贡献任何事。你可以很轻松地自己去执行擦除,生成没有泛型的类:

class Manipulator3 {
    private HasF obj;

    Manipulator3(HasF x) {
        obj = x;
    }

    public void manipulate() {
        obj.f();
    }
}

这提出了很重要的一点:泛型只有在类型参数比某个具体类型(以及其子类)更加“泛化”——代码能跨多个类工作时才有用。因此,类型参数和它们在有用的泛型代码中的应用,通常比简单的类替换更加复杂。但是,不能因此认为使用 <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 1.0 就含有泛型的话,那么这个特性就不会使用擦除来实现——它会使用具体化,保持参数类型为第一类实体,因此你就能在类型参数上执行基于类型的语言操作和反射操作。本章稍后你会看到,擦除减少了泛型的泛化性。泛型在 Java 中仍然是有用的,只是不如它们本来设想的那么有用,而原因就是擦除。

在基于擦除的实现中,泛型类型被当作第二类类型处理,即不能在某些重要的上下文使用泛型类型。泛型类型只有在静态类型检测期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如, List<T> 这样的类型注解会被擦除为 List,普通的类型变量在未指定边界的情况下会被擦除为 Object

擦除的核心动机是你可以在泛化的客户端上使用非泛型的类库,反之亦然。这经常被称为“迁移兼容性”。在理想情况下,所有事物将在指定的某天被泛化。在现实中,即使程序员只编写泛型代码,他们也必须处理 Java 5 之前编写的非泛型类库。这些类库的作者可能从没想过要泛化他们的代码,或许他们可能刚刚开始接触泛型。

因此 Java 泛型不仅必须支持向后兼容性——现有的代码和类文件仍然合法,继续保持之前的含义——而且还必须支持迁移兼容性,使得类库能按照它们自己的步调变为泛型,当某个类库变为泛型时,不会破坏依赖于它的代码和应用。在确定了这个目标后,Java 设计者们和从事此问题相关工作的各个团队决策认为擦除是唯一可行的解决方案。擦除使得这种向泛型的迁移成为可能,允许非泛型的代码和泛型代码共存。

例如,假设一个应用使用了两个类库 XYY 使用了类库 Z。随着 Java 5 的出现,这个应用和这些类库的创建者最终可能希望迁移到泛型上。但是当进行迁移时,它们有着不同的动机和限制。为了实现迁移兼容性,每个类库与应用必须与其他所有的部分是否使用泛型无关。因此,它们不能探测其他类库是否使用了泛型。因此,某个特定的类库使用了泛型这样的证据必须被”擦除“。

如果没有某种类型的迁移途径,所有已经构建了很长时间的类库就需要与希望迁移到 Java 泛型上的开发者们说再见了。类库毫无争议是编程语言的一部分,对生产效率有着极大的影响,所以这种代码无法接受。擦除是否是最佳的或唯一的迁移途径,还待时间来证明。

擦除的问题

因此,擦除主要的正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下将泛型融入到语言中。擦除允许你继续使用现有的非泛型客户端代码,直至客户端准备好用泛型重写这些代码。这是一个崇高的动机,因为它不会骤然破坏所有现有的代码。

擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操作中,例如转型、instanceof 操作和 new 表达式。因为所有关于参数的类型信息都丢失了,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有有关参数的类型信息而已。

考虑如下的代码段:

class Foo<T> {
    T var;
}

看上去当你创建一个 Foo 实例时:

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

class Foo 中的代码应该知道现在工作于 Cat 之上。泛型语法也在强烈暗示整个类中所有 T 出现的地方都被替换,就像在 C++ 中一样。但是事实并非如此,当你在编写这个类的代码时,必须提醒自己:“不,这只是一个 Object“。

另外,擦除和迁移兼容性意味着,使用泛型并不是强制的,尽管你可能希望这样:

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 {
} // No warning

// class Derived3 extends GenericBase<?> {}
// Strange error:
// unexpected type
// required: class or interface without bounds
public class ErasureAndInteritance {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        Derived2 d2 = new Derived2();
        Object obj = d2.get();
        d2.set(obj); // Warning here!
    }
}

Derived2 继承自 GenericBase,但是没有任何类型参数,编译器没有发出任何警告。直到调用 set() 方法时才出现警告。

为了关闭警告,Java 提供了一个注解,我们可以在列表中看到它:

@SuppressWarnings("unchecked")

这个注解放置在产生警告的方法上,而不是整个类上。当你要关闭警告时,最好尽可能地“聚焦”,这样就不会因为过于宽泛地关闭警告,而导致意外地遮蔽掉真正的问题。

可以推断,Derived3 产生的错误意味着编译器期望得到一个原生基类。

当你希望将类型参数不仅仅当作 Object 处理时,就需要付出额外努力来管理边界,并且与在 C++、Ada 和 Eiffel 这样的语言中获得参数化类型相比,你需要付出多得多的努力来获得少得多的回报。这并不是说,对于大多数的编程问题而言,这些语言通常都会比 Java 更得心应手,只是说它们的参数化类型机制相比 Java 更灵活、更强大。

边界处的动作

因为擦除,我发现了泛型最令人困惑的方面是可以表示没有任何意义的事物。例如:

import java.lang.reflect.*;
import java.util.*;

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>,擦除也意味着它实际被存储为没有任何参数的 Class。因此,当你在使用它时,例如创建数组,Array.newInstance() 实际上并未拥有 kind 所蕴含的类型信息。所以它不会产生具体的结果,因而必须转型,这会产生一条令你无法满意的警告。

注意,对于在泛型中创建数组,使用 Array.newInstance() 是推荐的方式。

如果我们创建一个集合而不是数组,情况就不同了:

import java.util.*;

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(),编译器就会发出警告。

本例中这么做真的毫无意义吗?如果在创建 List 的同时向其中放入一些对象呢,像这样:

import java.util.*;
import java.util.function.*;

public class FilledList<T> extends ArrayList<T> {
    FilledList(Supplier<T> gen, int 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 version:
        List<Integer> ilist = new FilledList<>(() -> 47, 4);
        System.out.println(ilist);
    }
}

在这里插入图片描述

即使编译器无法得知 add() 中的 T 的任何信息,但它仍可以在编译期确保你放入 FilledList 中的对象是 T 类型。因此,即使擦除移除了方法或类中的实际类型的信息,编译器仍可以确保方法或类中使用的类型的内部一致性。

因为擦除移除了方法体中的类型信息,所以在运行时的问题就是_边界_:即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。

考虑如下这段非泛型示例:

public class SimpleHolder {
    private Object obj;

    public void set(Object obj) {
        this.obj = obj;
    }

    public Object get() {
        return obj;
    }

    public static void main(String[] args) {
        SimpleHolder holder = new SimpleHolder();
        holder.set("Item");
        String s = (String) holder.get();
    }
}

如果用 javap -c SimpleHolder 反编译这个类,会得到如下内容(经过编辑):

在这里插入图片描述

set()get() 方法存储和产生值,转型在调用 get() 时接受检查。

现在将泛型融入上例代码中:

public class GenericHolder2<T> {
    private T obj;

    public void set(T obj) {
        this.obj = obj;
    }

    public T get() {
        return obj;
    }

    public static void main(String[] args) {
        GenericHolder2<String> holder = new GenericHolder2<>();
        holder.set("Item");
        String s = holder.get();
    }
}

get() 返回后的转型消失了,但是我们还知道传递给 set() 的值在编译期会被检查。下面是相关的字节码:

在这里插入图片描述

所产生的字节码是相同的。对进入 set() 的类型进行检查是不需要的,因为这将由编译器执行。而对 get() 返回的值进行转型仍然是需要的,只不过不需要你来操作,它由编译器自动插入,这样你就不用编写(阅读)杂乱的代码。

get()set() 产生了相同的字节码,这就告诉我们泛型的所有动作都发生在边界处——对入参的编译器检查和对返回值的转型。这有助于澄清对擦除的困惑,记住:“边界就是动作发生的地方”。

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

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

相关文章

深入理解强化学习——多臂赌博机:乐观初始值

分类目录&#xff1a;《深入理解强化学习》总目录 目前为止我们讨论的所有方法都在一定程度上依赖于初始动作值 Q 1 ( a ) Q_1(a) Q1​(a)的选择。从统计学角度来说&#xff0c;这些方法&#xff08;由于初始估计值&#xff09;是有偏的。对于采样平均法来说&#xff0c;当所有…

软件专业毕业生的如何找工作?——加速度jsudo

据统计&#xff0c;2023届全国高校毕业生预计达到1158万人&#xff0c;同比增长82万人。根据某大学的统计数据&#xff0c;IT专业的就业率在过去五年中保持了稳定增长的趋势&#xff0c;平均超过90%。 IT行业的薪资水平相对较高&#xff0c;也让很多高校和培训机构愿意投入更多…

Sybase连接详解

Sybase连接详解 Sybase连接详解摘要一、JDBC基础1.1 JDBC简介1.2 JDBC驱动程序 二、配置Sybase JDBC连接2.1 连接Sybase数据库2.2 验证Sybase JDBC连接2.3 获取Sybase数据库表信息和注释2.4 根据表名获取Sybase字段信息和注释2.5 执行SQL查询2.6 插入数据2.7 执行Sybase存储过程…

ElasticSearch离线安装

1. 上传和解压软件 将elasticsearch-7.11.2-linux-x86_64.tar.gz和kibana-7.11.2-linux-x86_64.tar.gz 上传到/data/es目录 解压文件 tar -zxvf elasticsearch-7.11.2-linux-x86_64.tar.gz tar -zxvf kibana-7.11.2-linux-x86_64.tar.gz 2. 创建es用户 因为安全问题&#xff…

windows好玩的cmd命令

颜色 后边的数字查表吧,反正我是喜欢一个随机的数字 color 01MAC getmac /v更新主机IP地址 通过DHCP更新 ipconfig /release ipconfig /renew改标题 title code with 你想要的标题

如何实现Word文档中的书签双向定位

工作中&#xff0c;经常需要拟定合同&#xff0c;一般都有固定的模板&#xff0c;在特定的位置填写内容。通过zOffice编辑合同文件时&#xff0c;可以在模板需要填写的位置预设书签&#xff0c;配合zOffice SDK使用&#xff0c;利用zOffice书签双向定位的特性&#xff0c;更方便…

3.5、Linux:命令行git的使用

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 在Linux Centos7.6下安装git yum -y install git 注册一个gitee账号 进去注册就好&#xff0c;记住自己的用户名和密码。 创建一个仓库 点击复制&#xff0c;接着就可以在Linux上使用了 git clone git clone 刚才复制的地…

《UML和模式应用(原书第3版)》2024新修订译本部分截图

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 机械工业出版社即将在2024春节前后推出《UML和模式应用&#xff08;原书第3版&#xff09;》的典藏版。 受出版社委托&#xff0c;UMLChina审校了原中译本并做了一些修订。同比来说&a…

Qwt QwtThermo绘制温度计

1.简介 QwtThermo 是一个基于 Qt 框架的类库&#xff0c;用于创建温度计控件。它提供了一些方便的功能来展示和处理温度计相关的数据。 QwtThermo 添加了特定于温度计的功能。 使用 QwtThermo&#xff0c;可以实现以下功能&#xff1a; 设置温度范围&#xff1a;可以通过设置…

golang正则获取中括号中的内容

reg : regexp.MustCompile("【(.*?)】") //userInfo姓名:【AAA姓名】证件类型:【BBB身份证】证件号码:【122456789458】tempData reg.FindAllStringSubmatch(userInfo, -1)for k, v : range tempData {if k 0 {tempReleaseUser.Name v[1]//AAA姓名} else if k 1…

选择企业云盘?品牌推荐和评价解析

企业云盘是如今热门的企业协作工具&#xff0c;为企业提供了文件存储、文件共享服务。市面上的企业云盘千千万&#xff0c;到底哪个企业云盘好用&#xff1f;哪些品牌值得信赖呢&#xff1f; 好用的企业云盘&#xff0c;不能不提&#xff0c;Zoho Workdrive企业云盘为企业提供…

养老院展示服务预约小程序的作用是什么

养老院无论在哪个城市都有很高需求度&#xff0c;不少银发人群会因为种种原因而前往&#xff0c;而养老院近些年来各种服务也比较完善&#xff0c;增加了客户信任度及接受度&#xff0c;但对院方来说&#xff0c;也存在着一些痛点&#xff1a; 1、品牌传播服务呈现难 养老院也…

自己实现一个自动检测网卡状态,并设置ip地址

阅读本文前&#xff0c;请先学习下面几篇文章 《搞懂进程组、会话、控制终端关系&#xff0c;才能明白守护进程干嘛的&#xff1f;》 《简简单单教你如何用C语言列举当前所有网口&#xff01;》 《Linux下C语言操作网卡的几个代码实例&#xff01;特别实用》 《安卓如何设置…

使用 Python 从头开始​​编写 Canny 边缘检测算法。

原始图像&#xff08;左&#xff09;和检测到的边缘&#xff08;右&#xff09;| 图片由作者提供 一、说明 在本文中&#xff0c;我将解释有关 Canny 边缘检测的所有内容&#xff0c;以及在不使用一些预先编写的库的情况下对算法进行编码&#xff0c;以便您能够了解真正发生的情…

CSS 浮动

目标target✓ 能够说出来为什么需要浮动能够说出来浮动的排列特性能够说出来三种最常见的布局方式能够说出来为什么需要清除浮动,能够至少写出两种清楚浮动的方法能够利用Photoshop实现基本的切图能够利用Photoshop插件实现切图能够完成学成在线的页面布 传统网页布局的三种模…

uniapp项目启动时检查版本,版本过旧提示:更新至最新版本,App简单版实现思路详解

效果&#xff1a; 实现的思路比较简单&#xff0c;后期需要优化的话&#xff0c;会持续更新 uniapp文档上有提到关于版本更新 在文档中搜uni.getUpdateManager()&#xff08;小程序的更新&#xff09;&#xff0c;会提示app的更新需要点击连接&#xff0c;跳转到其他页面查看…

iOS 16.4 之后真机与模拟器无法使用Safari调试H5页面问题

背景 iOS 16.4之后用真机调试H5时候发现&#xff0c;Safari中开发模块下面无法调试页面 解决方案 在WKWebView中设置以下代码解决 if (available(iOS 16.4, *)) {[_webView setInspectable:YES];}然后再次调试就可以了

Tcl语言:SDC约束命令create_generated_clock详解(上)

相关阅读 Tcl语言https://blog.csdn.net/weixin_45791458/category_12488978.html?spm1001.2014.3001.5482 有时候&#xff0c;复杂的设计需要多个时钟来完成相应的操作&#xff0c;当设计中有多个时钟存在时&#xff0c;它们需要相互协作或各司其职。有几种时钟可能由其他时…

Centos7部署Python3环境

一、安装Python3 ###查看是否安装Python3 Centos 7 默认安装了python 2.7.5. 因为一些命令要用它比如yum 它使用的是python2.7.5 使用 python -V 命令查看一下是否安装Python 然后使用命令 which python 查看一下Python可执行文件的位置 Python指向的是Python2.7 安装依赖 y…

漏刻有时百度地图API实战开发(1)华为手机无法使用addEventListener click 的兼容解决方案

现象 漏刻有时项目开发中的调用了百度地图API&#xff0c;在PC端、IOS和安卓机型测试都没有问题。但是使用华为手机部分型号时&#xff0c;前端在监听点击事件的时候是使用 map.addEventListener(click,function(){...})&#xff0c;无法触发。或 原理 通过监听touchstart和…