初识Java 18-3 泛型

目录

边界

通配符

编译器的能力范畴

逆变性

无界通配符

捕获转换


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


边界

        在泛型中,边界的作用是:在参数类型上增加限制。这么做可以强制执行应用泛型的类型规则,但还有一个更重要的潜在效果,我们可以调用边界类型上的方法了。

    若一个泛型参数没有边界,那么我们只能调用其中的Object方法。

        为了应用边界的限制,Java复用了extend关键字:

【例子:使用extend规定泛型边界】

interface HasColor {
    java.awt.Color getColor();
}

class WithColor<T extends HasColor> {
    T item;

    WithColor(T item) {
        this.item = item;
    }

    T getItem() {
        return item;
    }

    // 可以调用位于边界上的方法:
    java.awt.Color color() {
        return item.getColor();
    }
}

class Coord {
    public int x, y, z;
}

// 在规定边界时,需要将类排(Coord)在前面,接口(HasColor)排在后面\
// 因此这种写法会失败:
// class WithColorCoord<T extends HasColor & Coord> {}

// 这样才能正确定义多重边界:
class WithColorCoord<T extends Coord & HasColor> {
    T item;

    WithColorCoord(T item) {
        this.item = item;
    }

    T getItem() {
        return item;
    }

    java.awt.Color color() {
        return item.getColor();
    }

    int getX() {
        return item.x;
    }

    int getY() {
        return item.y;
    }

    int getZ() {
        return item.z;
    }
}

interface Weight {
    int weight();
}

// 与继承一样,只能继承一个具体类,但可以实现多个接口:
class Solid<T extends Coord & HasColor & Weight> {
    T item;

    Solid(T item) {this.item = item;}

    T getItem() {return item;}

    java.awt.Color color() {return item.getColor();}

    int getX() {return item.x;}

    int getY() {return item.y;}

    int getZ() {return item.z;}

    int weight() {return item.weight();}
}

class Bounded extends Coord implements HasColor, Weight {
    @Override
    public java.awt.Color getColor() {
        return null;
    }

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

public class BasicBounds {
    public static void main(String[] args) {
        Solid<Bounded> solid =
                new Solid<>(new Bounded());
        solid.color();
        solid.getY();
        solid.weight();
    }
}

        可以通过继承去除上例中的一些冗余代码。继承也可以增加边界的限制:

【例子:使用继承简化代码】

class HoldItem<T> {
    T item;

    HoldItem(T item) {
        this.item = item;
    }

    T getItem() {
        return item;
    }
}

class WithColor2<T extends HasColor>
        extends HoldItem<T> {
    WithColor2(T item) {
        super(item);
    }

    java.awt.Color color() {
        return item.getColor();
    }
}

class WithColorCoord2<T extends Coord & HasColor>
        extends WithColor2<T> {
    WithColorCoord2(T item) {
        super(item);
    }

    int getX() {
        return item.x;
    }

    int getY() {
        return item.y;
    }

    int getZ() {
        return item.z;
    }
}

class Solid2<T extends Coord & HasColor & Weight>
        extends WithColorCoord2<T> {
    Solid2(T item) {
        super(item);
    }

    int weight() {
        return item.weight();
    }
}

public class InheritBounds {
    public static void main(String[] args) {
        Solid2<Bounded> solid2 =
                new Solid2<>(new Bounded());
        solid2.color();
        solid2.getY();
        solid2.weight();
    }
}

        Solid2变得更加简洁了。

        在这里,每一层的继承都会为对应的类增加边界的限制,同时继承那些来自父类的方法。这样我们就不需要在每个类中重复定义那些代码了。

        另外,创建泛型集合时需要注意,我们可以且只可以继承一个接口或类:

// 可以进行的操作:
List<? extends Coord> list;
List<? extends HasColor> list1;
// 不可行的操作:
// List < ? extends HasColor & Weight > list2;

通配符

        先看一个例子,将派生类的数组赋值给基类数组的引用:

【例子:数组的特殊行为】

class Fruit {
}

class Apple extends Fruit {
}

class Jonathan extends Apple {
}

class Orange extends Fruit {
}

public class CovariantArrays {
    public static void main(String[] args) {
        Fruit[] fruit = new Apple[10];
        // 可行的操作:
        fruit[0] = new Apple();
        fruit[1] = new Jonathan();
        // 但运行时的类型是Apple[],而不是Fruit[]或Orange[]

        try {
            // 编译器允许添加Fruit(父类):
            fruit[0] = new Fruit();
        } catch (Exception e) { // 但这种操作却会导致ArrayStoreException异常
            System.out.println(e);
        }

        try {
            // 编译器允许添加Orange:
            fruit[0] = new Orange();
        } catch (Exception e) { // 但同样会发生异常
            System.out.println(e);
        }
    }
}

        程序执行的结果是:

        在这个例子中,我们将派生类Apple的数组赋值给了Fruit数组:

Fruit[] fruit = new Apple[10];

这在继承结构上是合理的。

        不过需要注意一点,因为实际的数组类型是Apple[],所以将基类Fruit放入其中是不合理的编译器允许了这个行为,因为从代码上看,这只不过是将Fruit对象赋给了Fruit数组。数组机制能够知道数组的实际类型,因此才会在运行时抛出异常。

    数组可以维持其包含的对象的类型规则,这也是为什么上例这种类似“向上转型”的操作能够成功的原因。它在一定程度上能够确保我们不会乱用数组。

        尽管我们能够在运行时发现这种不合理的数组赋值。但使用泛型,我们可以在编译时提前进行错误检测:

【例子:泛型的编译时检查】

import java.util.ArrayList;
import java.util.List;

public class NonCovariantGenerics {
    List<Fruit> flist = new ArrayList<Apple>();
}

        编译器会在编译时发现如下的问题:

它告诉我们,我们无法将包含Apple的泛型赋值给包含Fruit的泛型。

        之所以会这样,是因为编译器无法掌握足够的信息,它并不知道List<Fruit>List<Apple>是什么关系(另外,这种关系也不会涉及向上转型,二者并不等价)。

        可以发现,在这里我们需要讨论的是集合自身的类型,而不是集合持有的元素类型。

    与数组不同,泛型并没有内建的协变性。数组完全由语言自身定义,而泛型的定义却来自于程序员。因此,编译器和运行系统有足够的信息来检查数组,却无法对泛型做到相同的事。

        若一定需要在List<Fruit>List<Apple>之间建立什么关系,可以使用通配符:

【例子:使用通配符建立关系】

import java.util.ArrayList;
import java.util.List;

public class NonCovariantGenerics {
    public static void main(String[] args) {
        // 可以用通配符提供协变的能力:
        List<? extends Fruit> flist = new ArrayList<>();

        // 但却不能添加任何类型的数据
        // flist.add(new Apple());
        // flist.add(new Fruit());
        // flist.add(new Object());
        flist.add(null); // 可以添加null,但没什么用

        // 至少能返回一个Fruit对象:
        Fruit f = flist.get(0);
    }
}

        显然,这并不意味着flist真的会持有任何Fruit类型,因为<? extends Fruit>实际上表示的是“某种继承自Fruit的类型”。这里存在着一个矛盾:

集合应该持有具体的类型,但flist只要求提供一种没有被确切指定的类型。

换言之,flist所要求的类型并不具体(这是为了能够向上转型为flist做出的牺牲)

    若一个集合并不要求所持有的类型足够具体,这个集合就会失去意义。而若我们并不知道集合持有的具体元素是什么,我们也无法安全地向其中添加元素。

        因为这种限制,通配符并不适合用于传入参数的集合。但我们可以将其用于接收一个已经打包好的集合,并从中取出元素。

编译器的能力范畴

        按照上面的说法,若使用了通配符,我们似乎无法调用一个带有参数的集合方法了。先看看这个例子:

【例子:调用泛型集合中的含参方法】

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

public class CompilerIntelligence {
    public static void main(String[] args) {
        List<? extends Fruit> flist =
                Arrays.asList(new Apple());
        Apple a = (Apple) flist.get(0); // 未产生警告
        // 方法中的参数是Object:
        flist.contains(new Apple());
        // 同样,参数也是Object:
        flist.indexOf(new Apple());
    }
}

        程序能够顺利执行。这似乎与之前得出的结论相悖——我们可以调用含参的集合方法。这是否是编译器在其中进行调度呢?

        答案是否定的,可以观察contains()indexOf()方法的参数列表:

contains()indexOf()的参数都是Object的,假若我们调用了flist.add()方法,则会发现:

因为此时add()方法是参数已经变成了? extends Fruit。编译器不会知道应该处理哪种具体的Fruit类型,因此不会接受任何类型。

        这里体现了一种思路:作为泛型类的设计者,若我们认为某种调度是“安全的”,那么可以将Object作为其的参数。例如:

【例子:设置“安全”调度的参数】

import java.util.Objects;

public class Holder<T> {
    private T value;

    public Holder() {
    }

    public Holder(T val) {
        value = val;
    }

    public void set(T val) {
        value = val;
    }

    public T get() {
        return value;
    }

    // 使用Object作为参数
    @Override
    public boolean equals(Object o) {
        return o instanceof Holder &&
                Objects.equals(value, ((Holder) o).value);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(value);
    }

    public static void main(String[] args) {
        Holder<Apple> apple = new Holder<>(new Apple());
        Apple d = apple.get();
        apple.set(d);

        // 不允许这种操作:
        // Holder<Fruit> fruit = apple;
        // 但允许这种操作:
        Holder<? extends Fruit> fruit = apple;
        Fruit p = fruit.get();
        d = (Apple) fruit.get(); // 返回一个Object,然后再转型
        try {
            Orange c = (Orange) fruit.get();
        } catch (Exception e) {
            System.out.println(e);
        }
        // 无法这样调用set():
        // fruit.set(new Apple());
        // fruit.set(new Fruit());
        System.out.println(fruit.equals(d));
    }
}

        程序执行的结果是:

        可以看到,Holder<Apple>无法向上转型为Holder<Fruit>,但却可以向上转型为Holder<? extends Fruit>get()方法和set()方法的使用都会受编译器的限制,值得一提的是,因为get()方法返回了Fruit,所以我们可以手动进行向下转型。

        因为equals()方法接受Object,所以它不会受到上述的限制。


逆变性

        除extends之外,还可以使用超类通配符(重写了super关键字)。如果说extends关键字可以为泛型添加限制,那么super就是为通配符添加了边界限制,其中的边界限制就是某个类的基类。例如:

<? super MyClass>
<? super T> // 可以使用类型参数
// <T super MyClass> // 但无法为泛型参数设置超类边界

        有了超类通配符,就可以向集合中进行写操作了:

【例子:向泛型集合中进行写操作】

import java.util.List;

public class SuperTypeWildcards {
    static void writeTo(List<? super Apple> apples) {
        apples.add(new Apple());
        apples.add(new Jonathan());
        // 但不可以添加基类元素:
        // apples.add(new Fruit());
    }
}

        我们可以向apples类型中添加Apple及其的子类型。但由于apples的下界是Apple,所以我们无法安全地先这个泛型集合中添加Fruit

【例子:总结一下通配符】

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

public class GenericReading {
    static List<Apple> apples =
            Arrays.asList(new Apple());
    static List<Fruit> fruit =
            Arrays.asList(new Fruit());

    // 调用精确的类型:
    static <T> T readExact(List<T> list) {
        return list.get(0);
    }

    // 兼容各种调用的静态方法:
    static void f1() {
        Apple a = readExact(apples);
        Fruit f = readExact(fruit);
        f = readExact(apples);
    }

    // 类的类型会在其实例化后确定:
    static class Reader<T> {
        T readExact(List<T> list) {
            return list.get(0);
        }
    }

    static void f2() {
        Reader<Fruit> fruitReader = new Reader<>();
        Fruit f = fruitReader.readExact(fruit);

        // fruitReader的参数类型是Fruit
        // 因此不会接受List<Apple>:
        // Fruit a = fruitReader.readExact(apples);
    }

    // 允许协变:
    static class CovariantReader<T> {
        T readCovariant(List<? extends T> list) {
            return list.get(0);
        }
    }

    static void f3() {
        CovariantReader<Fruit> fruitReader =
                new CovariantReader<>();
        Fruit f = fruitReader.readCovariant(fruit);
        Fruit a = fruitReader.readCovariant(apples);
    }

    public static void main(String[] args) {
        f1();
        f2();
        f3();
    }
}

        f1()使用了一个静态的泛型方法readExact()。从f1()中的调用可以看出,readExact()可以兼容不同的方法调用。因此,若可以使用静态的泛型方法,则不一定需要使用到协变。

        从f2()中可以看出,泛型类的对象会在被实例化时确定下来。因此fruitReader的类型参数被确定成了Fruit


无界通配符

        无界通配符<?>表示“一个泛型可以持有任何类型”,但在更多时候它是一种装饰,告诉别人我考虑过Java泛型,并确定此处的这个泛型可以适配任何类型。

【例子:无界通配符的使用】

import java.util.HashMap;
import java.util.Map;

public class UnboundedWildcards2 {
    static Map map1;
    static Map<?, ?> map2;
    static Map<String, ?> map3;

    static void assign1(Map map) {
        map1 = map;
    }

    static void assign2(Map<?, ?> map) {
        map2 = map;
    }

    static void assign3(Map<String, ?> map) {
        map3 = map;
    }

    public static void main(String[] args) {
        assign1(new HashMap());
        assign2(new HashMap());
        // 出现警告:
        assign3(new HashMap());

        assign1(new HashMap<>());
        assign2(new HashMap<>());
        assign3(new HashMap<>());
    }
}

        第一次调用的assign3()会会发出警告,可以在编译时添加-Xlint:unchecked来观察这个警告:

编译器似乎不会区分MapMap<?, ?>。下面的例子会展示出一点区别:

【例子:无界通配符带来的区别】

import java.util.ArrayList;
import java.util.List;

public class UnboundedWildcards1 {
    static List list1;
    static List<?> list2;
    static List<? extends Object> list3;

    static void assign1(List list) {
        list1 = list;
        list2 = list;

        // 会引发警告:
        list3 = list;
    }

    static void assign2(List<?> list) {
        list1 = list;
        list2 = list;
        list3 = list;
    }

    static void assign3(List<? extends Object> list) {
        list1 = list;
        list2 = list;
        list3 = list;
    }

    public static void main(String[] args) {
        assign1(new ArrayList());
        assign2(new ArrayList());

        // 也会引发警告:
        assign3(new ArrayList());

        assign1(new ArrayList<>());
        assign2(new ArrayList<>());
        assign3(new ArrayList<>());

        // 两种定义都被List<?>接受
        List<?> wildList = new ArrayList();
        wildList = new ArrayList<>();
        assign1(wildList);
        assign2(wildList);
        assign3(wildList);
    }
}

        这段代码也会触发一些警告:

这里体现了编译器对List<?>List<? extends Object>在处理上的不同。

        编译器似乎并不关心ListList<?>之间有何区别,因此对它们的处理才会如此相同。然而,尽管这二者都可以被看做List<Object>,但在细节上它们仍有区别,它们实际的指代如下:

  • List:实际上表示“持有任何Object类型的原生List”。
  • List<?>:持有某种具体类型的非原生List(不过我们并不知道具体类型是什么)。

        不过,在一些情况下,编译器仍会区分二者:

【例子:区分不同的泛型】

public class Wildcards {
    static void rawArgs(Holder holder, Object arg) {
        // 会触发警告:
        holder.set(arg);

        // 当前作用域中也没有T,所以不能这样写:
        // T t = holder.get();

        // 可以这么写,但会丢失类型信息:
        Object obj = holder.get();
    }

    // 与rawArgs()不同,方法会触发报错:
    static void unboundedArg(Holder<?> holder, Object arg) {
        // 发生报错:
        // holder.set(arg);

        // 当然,这样依旧不行:
        // T t = holder.get();

        // 可以,但还是会丢失类型信息:
        Object obj = holder.get();
    }

    static <T> T exact1(Holder<T> holder) {
        return holder.get();
    }

    static <T> T exact2(Holder<T> holder, T arg) {
        holder.set(arg);
        return holder.get();
    }

    static <T>
    T wildSubtype(Holder<? extends T> holder, T arg) {
        // 依旧会发生报错:
        // holder.set(arg);

        return holder.get();
    }

    static <T>
    void wildSupertype(Holder<? extends T> holder, T arg) {
        // 引发报错:
        // holder.set(arg);

        Object obj = holder.get();
    }

    public static void main(String[] args) {
        Holder raw = new Holder<>();
        // 这种写法也一样:
        raw = new Holder();

        Holder<Long> qualified = new Holder<>();
        Holder<?> unbounded = new Holder<>();
        Holder<? extends Long> bounded = new Holder<>();
        Long lng = 1L;

        rawArgs(raw, lng);
        rawArgs(qualified, lng);
        rawArgs(unbounded, lng);
        rawArgs(bounded, args);

        unboundedArg(raw, lng);
        unboundedArg(qualified, lng);
        unboundedArg(unbounded, lng);
        unboundedArg(bounded, lng);

        // 引发警告:
        Object r1 = exact1(raw);
        Long r2 = exact1(qualified);
        Object r3 = exact1(unbounded); // 方法返回Object类型
        // 引发异常:
        // Long r4 = exact1(bounded);

        // 引发警告
        Long r5 = exact2(raw, lng);
        Long r6 = exact2(qualified, lng);
        // 引发报错:
        // Long r7 = exact2(unbounded, lng);
        // 同样会报错:
        // Long r8 = exact2(bounded, lng);

        // 引发警告:
        Long r9 = wildSubtype(raw, lng);

        Long r10 = wildSubtype(qualified, lng);
        // 同样会获得Object类型
        Object r11 = wildSubtype(unbounded, lng);

        // 引发异常:
        Long r12 = wildSubtype(bounded, lng);

        // 引发警告:
        wildSupertype(raw, lng);

        wildSupertype(qualified, lng);

        wildSupertype(bounded, lng);
    }
}

        先看rawArgs中的holder.set(),编译时会产生警告:

由于这里使用的是Holder的原始类型,所以任何向set()中传入的类型都会被向上转型为Object。编译器知道这种行为是不安全的,所以发出了警告。注意:使用原始类型,就意味着放弃了编译时检查

        再看unboundedArg()中的holder.set(),与原生的Holder不同,使用Holder<?>时编译器提示的警告级别是error

这是因为原生的Holder可以持有任何类型的组合,而Holder<?>只能持有由某种具体类型组合成的单类型集合,因此我们无法传入一个Object

        除此之外,exact1()exact2()也因为参数的不同而受到了不同的限制:

可以看到,exact2()所受的限制更大。

    若向一个有“具体的”泛型类型(即无通配符)参数的方法中传入原生类型,就会产生警告。这是因为具体参数所需的信息并不存在于原生类型中。


捕获转换

        <?>有一个特殊的用法:可以向一个使用了<?>的方法传入原生类型,编译器可能可以推断出具体的类型参数。这被称为捕获转换,通过这种方式,我们可以捕获未指定的通配符类型,将其转换成具体的类型:

【例子:捕获转换的使用例】

 

public class CaptureConversion {
    static <T> void f1(Holder<T> holder) {
        T t = holder.get();
        System.out.println(t.getClass().getSimpleName());
    }

    static void f2(Holder<?> holder) {
        f1(holder); // 捕获类型,并将具体的类型传入f1()中
    }

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        Holder raw = new Holder<>(1);
        // 若直接传入f1()中,会产生警告
        f1(raw);
        // 但使用f2()就不会出现警告
        f2(raw);

        Holder rawBasic = new Holder();
        // 会产生警告:
        rawBasic.set(new Object());
        // 也不会出现警告
        f2(rawBasic);
        // 即使向上转型为Holder<?>,依旧可以推断出具体类型:
        Holder<?> wildcarded = new Holder<>(1.0);
        f2(wildcarded);
    }
}

        程序执行的结果是:

        需要注意的是,捕获转换经适用于“在方法中必须使用确切类型”的情况。我们无法从f2()方法中返回T,因为对f2()而言,T是未知的(因此,若需要返回值,我们需要自己传入类型参数)

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

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

相关文章

vue el-table (固定列+滚动列)【横向滚动条】确定滚动条是在列头还是列尾

效果图&#xff1a; 代码实现&#xff1a; html&#xff1a; <script src"//unpkg.com/vue2/dist/vue.js"></script> <script src"//unpkg.com/element-ui2.15.14/lib/index.js"></script> <div id"app" style&quo…

实战JVM高CPU、内存问题分析定位

背景&#xff1a; 业务中台组件MOSC开展压测工作&#xff0c;并发场景下发现CPU使用率达到100%&#xff0c;虽然程序没有报错&#xff0c;但是这种情况显然已经达到性能瓶颈&#xff0c;对服务带来了验证的效能影响&#xff0c;所以针对该CPU问题必须进行详细的根因分析处理。…

浅谈Python中的鸭子类型和猴子补丁

文章目录 前言一、鸭子类型二、猴子补丁关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项目源码合集①Python工具包②Python实战案例③Python小游戏源码五、面试资料六、Python兼职渠道 前言 Python 开发者可能…

AdaBoost提升分类器性能

目录 AdaBoost算法原理 AdaBoost工作详情 初始权重分配 第一轮 第二轮 后续轮次 最终模型 AdaBoost的API解释 AdaBoost 对房价进行预测 AdaBoost 与决策树模型的比较 结论 AdaBoost算法原理 在数据挖掘中&#xff0c;分类算法可以说是核心算法&#xff0c;其中 Ada…

如何应用ChatGPT撰写、修改论文及工作报告,提供写作能力及优化工作??

如果我想让gpt从pdf文档中提取相关关键词的内容&#xff0c;可以怎么做呢&#xff1f;&#xff1f;我们评论区讨论 ChatGPT 在论文写作与编程方面也具备强大的能力。无论是进行代码生成、错误调试还是解决编程难题&#xff0c;ChatGPT都能为您提供实用且高质量的建议和指导&am…

flink和机器学习模型的常用组合方式

背景 flink是一个低延迟高吞吐的系统&#xff0c;每秒处理的数据量高达数百万&#xff0c;而机器模型一般比较笨重&#xff0c;虽然功能强大&#xff0c;但是qps一般都比较低&#xff0c;日常工作中&#xff0c;我们一般是如何把flink和机器学习模型组合起来一起使用呢? fli…

9.Docker的虚悬镜像-Dangling Image

1.虚悬镜像的概念 虚悬镜像 (Dangling Image) 指的是仓库名 (镜像名) 和标签 TAG 都是 的镜像。 2.构建本地虚悬镜像 这里我以unbuntu为例来说明。 2.1 编写Dockerfile文件 FROM ubuntu:22.042.2 根据Dockerfile文件构建虚悬镜像 docker build .上面这段命令&#xff0c…

C#开发的OpenRA游戏之属性RenderSprites(8)

C#开发的OpenRA游戏之属性RenderSprites(8) 本文开始学习RenderSprites属性,这个属性是跟渲染有关的,因此它就摄及颜色相关的内容,所以我们先来学习一下调色板,这是旧游戏的图片文件保存的格式,如果放在现代来看,不会再采用这种方法,毕竟现在存储空间变大,便宜了,并…

做流体分析需要知道的两大核心问题:内流和外流

SOLIDWORKS Flow Simulation 是直观的流体力学 (CFD) 分析软件&#xff0c;可以快速轻松的分析产品内部或外部流体的流动情况&#xff0c;以用来改善产品性能和功能。SOLIDWORKS Flow Simulation将专业的流体分析进行功能优化&#xff0c;让普通机械设计师也能进行流体力学分析…

【Linux系统编程二十】:(进程通信2)--命名管道/共享内存

【Linux系统编程二十】&#xff1a;命名管道/共享内存 一.命名管道1.创建管道2.打开管道3.进行通信(server/client) 二.共享内存1.实现原理2.申请内存3.挂接4.通信5.去关联6.释放共享内存7.特性&#xff1a; 一.命名管道 上一篇介绍的一个管道是没有名字的 因为你打开那个文件…

在Python中调用imageJ开发

文章目录 一、在ImageJ中进行Python开发二、在Python中调用imageJ开发2.1、简介2.2、环境配置2.3、测试一2.4、测试二 Python imageJ 解决方案&#xff0c;采坑记录 一、在ImageJ中进行Python开发 原生ImageJ仅支持JS脚本&#xff08;JAVAScript&#xff09;&#xff0c;而Im…

蓝桥杯物联网竞赛_STM32L071_2_继电器控制

Stm32l071原理图&#xff1a; PA11与PA12连接着UNL2803 ULN2803是一种集成电路芯片&#xff0c;通常被用作高电压和高电流负载的驱动器。 ULN2803是一个达林顿阵列&#xff0c;当输入引脚&#xff08;IN1至IN8&#xff09;被连接到正电源时&#xff0c;相应的输出引脚&#xff…

大数据-计算框架选型与对比

计算框架选型与对比 一、大数据平台二、计算框架分类1.批处理架构2.实时流处理架构3.流批一体处理架构 三、计算框架关键指标1.处理模式2.可伸缩性3.消息传递3.1 至少一次&#xff08;at least once&#xff09;3.2 至多一次&#xff08;ai most once&#xff09;3.3 恰好一次&…

Redis报错:JedisConnectionException: Could not get a resource from the pool

1、问题描述&#xff1a; redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool 2、简要分析&#xff1a; redis.clients.util.Pool.getResource会从JedisPool实例池中返回一个可用的redis连接。分析源码可知JedisPool 继承了 r…

【git】使用ssh

前言 git之前一直使用https&#xff0c;因为很方便随时随地都可以用。最近把代码托管到GitHub&#xff0c;使用https就使用不了。后面听同事说GitHub使用ssh是没问题的&#xff0c;就想着尝试一下。 git ssh配置 设置用户名和邮箱 git config --global use.name username g…

FFmpeg常用命令讲解及实战二

文章目录 前言一、ffmpeg 常用命令1、ffmpeg 的封装转换2、ffmpeg 的编转码3、ffmpeg 的基本编转码原理 二、ffprobe 常用参数1、show_format2、show_frames3、show_streams4、print_format5、select_streams 三、ffplay 的常用命令1、ffplay 常用参数2、ffplay 高级参数3、ffp…

教你看现货黄金实时报价

现货黄金投资市场上的交易软件众多&#xff0c;很多人不知道选择什么软件好&#xff0c;但选择主流软件MT4&#xff0c;基本就可以满足投资者不同的需求。本文为大家讲讲&#xff0c;为什么有那么多的投资者&#xff0c;都选择通过MT4获取实时的行情报价。 现货黄金市场波动激烈…

什么是网络爬虫技术?它的重要用途有哪些?

网络爬虫&#xff08;Web Crawler&#xff09;是一种自动化的网页浏览程序&#xff0c;能够根据一定的规则和算法&#xff0c;从互联网上抓取和收集数据。网络爬虫技术是随着互联网的发展而逐渐成熟的一种技术&#xff0c;它在搜索引擎、数据挖掘、信息处理等领域发挥着越来越重…

【MySQL】子查询

文章目录 子查询IN运算符子查询 VS 连接ALL关键字ANY关键字相关子查询 !EXISTS运算符select子句中的子查询from子句中的子查询 子查询 获取价格大于id为3的货物的商品 用到了内查询&#xff0c;获取id为3的商品的单价&#xff0c;把结构传给外查询 在where子句中编写子查询&am…

导数、方向导数、梯度方向、梯度

导数&#xff1a;自变量改变一定量时&#xff08;大于或小于0&#xff09;&#xff0c;因变量改变多少 方向导数&#xff1a;限定在某一个方向上&#xff0c;自变量改变一定量时&#xff08;大于0&#xff09;&#xff0c;因变量改变多少 梯度方向&#xff1a;方向导数最大的…