普通视图

Received before yesterday

基于Dify搭建AI智能体应用

2025年12月1日 00:00

1.Dify概述

Dify是一个低代码/无代码的AI应用开发平台,它通过可视化的智能体工作流将大型语言模型与你已有的工具和数据连接起来,你可以构建一个流程,让AI智能体自动完成一连串操作。

2.本地部署Dify

本文采用docker的方式本地部署dify 1.0.1,整个部署操作,都需要在全程国际联网的环境下进行,且要尽量保证docker中不运行其他的容器

2.1 安装docker-compose 2.x

dify的编排文件采用的是docker-compose 2.x版本规范,因此如果没有安装或者使用的是3.x版本,需要下载一个docker-compose 2.x

wget https://github.com/docker/compose/releases/download/v2.39.2/docker-compose-linux-x86_64 

下载完成后,放入/opt下

2.2 部署dify

先从github拉取dify源码到/opt/dify目录下

git clone https://github.com/langgenius/dify.git

切换到dify/docker目录下,将默认文件.env.example重命名复制一份

cd difycd dockercp .env.example .env

从dify/docker目录下,使用刚刚下载的docker-compose-linux-x86_64启动

/opt/docker-compose-linux-x86_64 up -d

第一次启动,需要下载许多镜像

当全部镜像下载完成后,会启动,直到全部启动成功

浏览器访问虚拟机地址的80,即可进入,第一次进入需要设置管理员用户名和密码

如果设置管理员时,弹窗提示无权限:

Setup failed: PermissionDenied (persistent) at write => permission denied Context: service: fs path: privkeys/5a438d1c-8c8b-43c2-a83e-1478fd3df017/private.pem Source: Permission denied (os error 13)

则需要返回到dify/docker目录内执行chmod -R 777 volumes/放开权限

成功注册管理员后,会进入主页面

2.3 配置大模型

先配置大模型,从主界面设置进入

需要安装OpenAI,DeepSeek等大模型应用,如果想要的大模型应用没有,可以使用OpenAI-API-compatible,前提是其适配了OpenAI的协议

安装完成后,将自己的API KEY填入对应的大模型应用中

3.智能体案例

待续

JVM开篇

2023年5月10日 00:00

持续更新

一、概述

JVM(Java Virtual Machine),即Java虚拟机,是Java语言跨平台的基础,是Java语言一次编译,到处运行的保障

如果说Java是跨平台的语言,那JVM就是个跨语言的平台。

JVM是安装在操作系统之上的,和硬件没有直接的交互

二、JVM的构成和工作原理

参考

  1. 《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》,作者:周志明,机械工业出版社,2019年
  2. 《剑指JVM》,作者:尚硅谷教育,清华大学出版社,2023年4月

Git

2024年9月24日 15:00

Git基本操作

1. 设置签名

  • 形式:用户名 + 邮箱地址

  • 级别

    • 系统用户:当前操作系统的用户内有效
    • 仓库级别:仅在本地仓库内有效
  • 作用范围:就近原则

    • 设置了仓库级别,就不使用系统级别
    • 只有系统级别存在时,才使用系统用户级别
  • 至少设置一个级别,否则无法正常工作

配置命令

  • 仓库级别

    git config user.name 用户名git config user.email 邮箱
  • 系统级别

    git config --global user.name 用户名git config --global user.email 邮箱

配置保存位置

  • 系统级别{home}\.gitconfig
  • 仓库级别{repo}\.git\config

2. 暂存文件

git add "文件名"  # 存放文件到暂存区

3. 撤回暂存区文件

git rm --cached <file>  # 将文件由暂存区撤回

4. 提交文件

git commit -m "提交信息"  # 提交一个文件

5. 移除版本控制

git rm -r --cached  # 移除版本控制,提交后生效

新建文件提交

  • 对于新建的文件,必须 git add 进入暂存区。
  • 如果之前已经添加进去了,修改后既可以先添加暂存区再提交,也可以直接:
    git commit -a -m "message"  # 直接提交

6. 查看版本历史

git log  # 查看版本历史纪录git log --oneline  # 简略显示git log --pretty=oneline  # 显示在一行,可读性强git reflog  # 显示切换到哪个版本的记录

日志多屏显示

  • 空格:向下翻页
  • b:向上翻页
  • q:退出

7. 版本回退

git reset --hard 索引值  # 强制回退到指定版本

使用符号回退

  • ^:只能回退
    git reset --hard HEAD^  # 当前版本前退一步
  • 波浪线:表示后退步数
    git reset --hard HEAD~3  # 前退三步

reset 的区别

  • soft:仅在本地库移动 HEAD 指针,不触碰暂存区和工作区
  • mixed:本地库移动指针,重置暂存区
  • hard:本地库移动指针,暂存区和工作区均会被操作

8. 文件差异

git diff 文件名  # 比较工作区与暂存区的差异git diff HEAD 文件名  # 比较工作区与本地库中的历史版本差异
  • 不带文件名会比较多个文件。

9. 分支管理

  • 查看所有分支

    git branch -a  # 查看所有分支git branch -r  # 查看远程分支git branch  # 查看本地分支
  • 删除分支

    git branch -D 分支名
  • 新建分支

    git branch 分支名
  • 切换分支

    git checkout 分支名
  • 创建并切换到新分支

    git checkout -b 分支名
  • 分支合并

    1. 先切换到接受修改的目标分支
    2. 执行:
      git merge 要合并的分支
    3. 如果有冲突,打开文件进行编辑
    4. 解决冲突后:
      git add 文件git commit  # 进行标记解决冲突

10. 远程库

  • 查看所有远程库别名

    git remote -v
  • 添加远程库别名

    git remote add 别名 地址
  • 推送到远程库

    git push 远程库别名 分支名
  • 拉取远程库内容

    git fetch gitlab master  # 拉取到本地,不修改工作区
  • 合并远程库修改

    git pull 远程库别名 分支名  # 拉取并合并

11. Git 原理

  • 哈希算法:使用不可逆算法(如 SHA-1),同一算法得到的长度相同。
  • Git 基于快照进行版本管理,而不是增量式管理。未修改的文件会指向前一个版本。

12. 标签管理

  • 删除标签

    git tag -d test_tag
  • 远端删除标签

    git push origin --tags

Java泛型

2022年9月21日 00:00

一、引言

泛型(Generics)和面向对象、函数式编程一样,也是一种程序设计的范式,泛型允许程序员在定义类、接口和方法时使用引用类型的类型形参代表一些以后才能确定下来的类型,在声明变量、创建对象、调用方法时像调用函数传参一样将具体类型作为实参传入来动态指明类型。

Java的泛型,是在jdk1.5中引入的一个特性,最主要应用是在jdk1.5的新集合框架中。作为Java语法层面的东西,本博客原本不打算介绍,但考虑到泛型理解和使用起来有一定的难度,应用的还很普遍,再加上自己工作多年好像也没有能够完全理解和灵活的运用泛型,因此还是决定看一些相关的书籍中与泛型有关的内容,并用一些篇幅总结下学习成果,介绍下我理解的泛型。

二、泛型类(接口)

2.1 创建泛型类

先来看两个类

public class StringPrinter {    private String thingsToPrint;    public StringPrinter() {    }    public StringPrinter(String thing) {        thingsToPrint = thing;    }    public void setThingsToPrint(String thing) {        thingsToPrint = thing;    }    public void print() {        System.out.println(thingsToPrint);    }}
public class IntegerPrinter {    private String thingsToPrint;    public IntegerPrinter() {    }    public IntegerPrinter(String thing) {        thingsToPrint = thing;    }    public void setThingsToPrint(String thing) {        thingsToPrint = thing;    }    public void print() {        System.out.println(thingsToPrint);    }}

两个类的作用相当,都是将传进来的参数进行打印,类的功能几乎完全相同,唯一的不同是参数的类型不一样,假如要为很多类型实现这个打印功能,就会编写很多的Printer类,如果要实现一个类统一实现这个功能,就可以采用泛型。

先来讲讲泛型语法,泛型用一个“菱形”<>声明,<>中是类型形参列表,如有多个类型形参,使用英文逗号,隔开。

下面程序定义了一个带有泛型声明的Printer类,有一个类型形参T(Type),声明了类的泛型参数后,就可以在类内部使用此泛型参数,构造函数名仍然是类名本身不需要加泛型

public class Printer<T> {    private T thingsToPrint;    public Printer() {            }    public void setThingsToPrint(T thing) {        thingsToPrint = thing;    }    public Printer(T thing) {        thingsToPrint = thing;    }    public void print() {        System.out.println(thingsToPrint);    }}

Java还能在定义类型参数时设置限制条件,如下例定义了一个NumberPrinter类,通过extends指定T的类型上限只能是Number。

⚠️注意:类型参数和第四章提到的类型通配符是不一样的,类型参数上的限制不能用super关键字,因为会造成不确定,使用extends指定T的类型上限,编译器至少知道T是个Number,如果是super关键字,编译器根本不知道T有哪些属性和方法。

public class NumberPrinter<T extends Number> {    private T thingsToPrint;    public NumberPrinter() {    }    public void setThingsToPrint(T thing) {        thingsToPrint = thing;    }    public NumberPrinter(T thing) {        thingsToPrint = thing;    }    public void print() {        System.out.println(thingsToPrint);    }    public T get() {        return thingsToPrint;    }}

还可以设置多个限制条件,extends后面只能有一个类但是可以有多个接口:

public class NumberPrinter<T extends Number & Comparable<T>> {    private T thingsToPrint;}

创建泛型接口同理,例如jdk中的List实际上就是一个接口。

public interface List<E> extends Collection<E> {    }

并非任何类都能声明为泛型类,Java规定:异常类(java.lang.Throwable)不得带有泛型

public class MyException<T> extends Exception { //编译出错❌,Generic class may not extend 'java.lang.Throwable'    T msg;}
public class MyException<T> extends RuntimeException { //编译出错❌,Generic class may not extend 'java.lang.Throwable'    T msg;}
public class MyException<T> extends Throwable { //编译出错❌,Generic class may not extend 'java.lang.Throwable'    T msg;}

2.2 实例化泛型类

使用泛型类创建对象时就可以为类型形参T传入具体类型,就可以生成类似Printer<String>Printer<Double>的类型

public static void main(String[] args) {    // 构造器T形参是String,只能用String初始化    Printer<String> printer1 = new Printer<String>("apple");    printer1.print(); //apple    // 构造器T形参是Double,只能用Double初始化    Printer<Double> printer2 = new Printer<Double>(3.8);    printer2.print(); //3.8}

jdk1.7以后,支持泛型类型推断,可以简写为:

Printer<String> printer1 = new Printer<>("apple");Printer<Double> printer2 = new Printer<>(3.8);

如不指定类型实参默认为Object类型,因为所有引用类型都能被Object代表,int、double、char等基本数据类型不能被Object代表,这就是类型实参必须是引用类型的原因,不过注意如果定义类型形参时通过entends指定了上限例如NumberPrinter<T extends Number>,则不传递类型实参时默认为上限类型Number

public static void main(String[] args) {    Printer printer1 = new Printer("apple");    printer1 = new Printer(12);    printer1 = new Printer(new Date());    NumberPrinter numberPrinter1 = new NumberPrinter(5);    NumberPrinter numberPrinter2 = new NumberPrinter(5.8);    NumberPrinter numberPrinter3 = new NumberPrinter(""); //编译出错❌}

2.3 派生泛型类

派生该类时,需要指定类型实参

public class HPPrinter extends Printer<Integer> {}

通过entends指定了上限的类型需不超过上限类型,以下同理

public class SuperNumberPrinter extends NumberPrinter<Double> {}public class SuperNumberPrinter extends NumberPrinter<Date> { //编译出错❌}

如不使用泛型,不指定类型实参,则泛型转换为Object类型或上限类型

public class HPPrinter extends Printer {    public static void main(String[] args) {        HPPrinter hpPrinter = new HPPrinter();        hpPrinter.setThingsToPrint(new Object());        hpPrinter.setThingsToPrint("hello");        hpPrinter.setThingsToPrint(12);    }}

还可以子类和父类声明同一个类型形参,子类中也不确定具体的类型,需要子类被实例化时将类型间接传递给父类,同时子类还可以一同定义自己的泛型

public class HPPrinter<T> extends Printer<T> {}
public class HPPrinter<T, E> extends Printer<T> {}

子类确定父类泛型类型的同时,又可以有自己的泛型

public class HPPrinter<E> extends Printer<Integer> {}

使用泛型又不指定类型的写法是错误的

public class HPPrinter extends Printer<T> { //编译出错❌}

三、泛型方法和泛型构造器

有时候,在类和接口上不需定义类型形参,只是具体方法中的某个类型不确定,需要在方法上面定义类型形参,这个也是支持的,jdk1.5提供了对于泛型方法的支持。

3.1 泛型方法

声明方法时,在返回值前指明泛型的类型形参列表<>,类型形参仅作用于方法内,这个方法就声明为了泛型方法。类型形参可以出现在参数和返回值中,调用方法时指定具体类型。泛型方法可以根据需要声明为静态。任何类中都可以存在泛型方法,而不是只有泛型类中才能声明泛型方法。

💡在返回值前面指明泛型的类型形参列表<>是泛型方法的特征,没有这个特征的都不是泛型方法,泛型类中使用类<>里面声明的类型作为方法参数或返回值类型的方法,不属于泛型方法,例如2.1中Printer类中的任何方法都不是泛型方法。

public class Demo {    public <T> E fun1(T e) {        return null;    }    public <T> void fun2(T e) {            }    public static <T> List<T> copyArray(T[] arr) {        List<T> list = new ArrayList<>();                for (int i = 0; i < arr.length; i++) {            list.add(arr[i]);        }                return list;    }    public <T> T getMiddle(T... a) {        return a[a.length / 2];    }    public static  <T> T getMiddleStatic(T... a) {        return a[a.length / 2];    }}

与类、接口中使用泛型参数不同的是,方法中的泛型参数无须显式传入实际类型参数,当程序调用copyArray()方法时,无须在调用该方法前传入String、Obiect等类型,但系统依然可以知道类型形参的数据类型,因为编译器根据实参推断类型形参的值,它通常推断出最直接的类型参数。例如,下面调用代码:

public static void main(String[] args) {    List<Dog> dogs = copyArray(new Dog[]{});}

如果要显示指明类型实参,则需要和实际类型一致,而且必须在对象名.this.类名.之后指定,否则语法报错。

public static void main(String[] args) {    Demo demo = new Demo();    Integer middle1 = getMiddleStatic(1, 2, 3);        String middle2 = Demo.<String>getMiddleStatic("a", "b", "c");    // 指定的和传入的类型不匹配,编译出错❌    Integer middle3 = Demo.<String> getMiddleStatic(1, 2, 3);    Double d = demo.<Double>getMiddleStatic(1.0, 2.0, 3.0);}

当使用自动推断时,如果涉及多个类型进行自动推断则取多个类型的共同父类(接口)

public static void main(String[] args) {    Number n1 = getMiddleStatic(1, 2, 3.9);    Serializable n2 = getMiddleStatic(1, 2, "", new Date());    Serializable n3 = getMiddleStatic( 2, "", null);}

3.2 泛型构造器

构造器也可能成为泛型方法,Java也允许在构造器签名中声明类型形参,这样就产生了所谓的泛型构造器。例如:

public class Demo {    public <T> Demo(T obj) {        System.out.println(obj.getClass());    }    public static void main(String[] args) {        new Demo("hello"); //class java.lang.String        new Demo(12); //java.lang.Integer        new <String> Demo("hello"); //class java.lang.String        new <Integer> Demo(12); //java.lang.Integer        new <String> Demo(12); // 编译出错❌    }}

如果泛型构造器上指明了类型形参,则不可以在new后使用“菱形”语法又手动指定,否则会导致类型无法推断。不过这种奇怪的写法应该不常碰见

public class Demo<T> {    public <T> Demo(T obj) {        System.out.println(obj.getClass());    }    public static void main(String[] args) {                // 类指定String 方法显示指定Integer        Demo<String> demo1 =  new <Integer> Demo<String>(4);        //类指定Integer 方法隐式指定String        Demo<Integer> demo2 = new Demo<Integer>("4");                //类型推断为Integer        Demo<Integer> demo3 = new Demo<>(3);                // 编译出错❌,不能既要求自动类型推断,又要手动指定        Demo<Integer> demo4 = new <String> Demo<>("4");    }}

四、不存在泛型类

包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态地生成无数多个逻辑上的子类,但这种子类在物理上并不存在。

即使加了不同泛型,运行时仍然是同一种类,并不会因为类型参数的不同,产生新的类

public static void main(String[] args) {    Fruit<String> fruit = new Fruit<>("apple");    Fruit<Double> fruit2 = new Fruit<>(3.8);    System.out.println(fruit2.getClass() == fruit.getClass()); //true}

因此在泛型类中的静态的代码块、静态变量和静态方法上,不能使用类型形参

public class Demo<T> {        public static T st; //编译出错❌        static {        T a = st;  //编译出错❌    }        public static void fun1(T obj) { //编译出错❌        st = obj;    }}

由于并不存在真正的泛型类,因此instanceof关键字后不能接泛型类

if (new ArrayList<>() instanceof List<String>) {  //编译出错❌            }if (new ArrayList<>() instanceof List) { //正确写法✅            }

事实上,泛型在编译后会被擦除,运行时Java虚拟机中没有泛型,只有普通类和普通方法,<T>会变为Object类型,<T extends Serializable>会变成Serializable类型

例如下面例子通过反射忽略了泛型,从而在运行时将一个List<String>中添加进去了一个Integer和Date类型的对象。

public class TestReflection {    public static void main(String[] args) {        List<String> strs = new ArrayList<>();        strs.add("hello");        strs.add("world");        Class clazz = strs.getClass();        try {            Method method = clazz.getMethod("add", Object.class);            method.invoke(strs, 1);            method.invoke(strs, new Date());        }        catch (Exception e) {            e.printStackTrace();        }        finally {            for (Object obj : strs) {                System.out.println(obj);            }        }    }}

运行结果

helloworld1Fri Apr 25 22:30:10 CST 2025

之前提到Java规定异常类不得带有泛型,原因就是异常在运行时是存在的,而泛型在运行时不存在,进行捕获处理时根本不能区分出来。

因为泛型运行时被擦除,因此泛型会影响方法的重载。例如下例由于List的泛型被擦除,导致两个方法不能重载。

public class Demo {    //编译出错❌ 'test(List<String>)' clashes with 'test(List<Integer>)'; both methods have same erasure    void test(List<String> list) {            }    void test(List<Integer> list) {    }}

五、类型通配符

因为泛型被大面积应用于Java集合,因此以List集合为例进行分析。

有时,如要实现一个遍历打印list的方法,list中是哪一种元素都有可能,于是我们将泛型实参指定为Object类型,看似解决了问题,但是调用时却会编译报错:无法将List<Object>用于List<String>

public class Demo {    public static void main(String[] args) {        List<String> strings = new ArrayList<>();        test(strings); //编译出错❌    }    public static void test(List<Object> list) {        for (int i = 0; i < list.size(); i++) {            System.out.println(list.get(i));        }    }}

在Java中,两个类通过继承和实现接口可以具有父子关系,但不能认为使用了父子类型的两个泛型类具有父子关系,例如上面程序出现了编译错误,说明List<String>不能被当成List<Object>的子类来用。

💡泛型与数组不同,如果是两个有父子关系的类各声明一个数组,例如:Object[]String[]String[]Object[]的子类型,是可以将String[]类型的变量赋值给Object[]的,这是一种Java语言早期不安全的设计,操作不当会引发ArrayStoreException,因此jdk1.5设计泛型时避免了这种设计。

为了表示任意类型,可以使用类型通配符,类型通配符是一个问号?,例如将一个问号作为类型实参传给List集合,写作List<?>,意思是元素类型未知的List。这个问号?被称为通配符,它的元素类型可以匹配任何类型。

现在使用任何类型的List来调用它,程序依然可以访问集合中的元素,其类型是Obiect,这永远是安全的,因为不管List的真实类型是什么,它包含的都是Obiect。

public static void test(List<?> list) {    Object obj = list.get(i);}

但是,如果调用add()方法向其中添加非null元素,又会发现编译出错

public static void test(List<?> list) {        list.add(new Date()); //编译出错❌    list.add(null); //能通过编译✅}

List.java

E get(int index);boolean add(E e);

通过分析List的get()add()两个方法的源码,可知get()方法是对泛型的读取,返回为EE类型虽然不确定,但肯定是一个Object类型,而add()方法需要为E类型赋值一个参数,是对泛型的写入,而传进来的?不能确定是什么类型,假如传进来的List是个List<String>,在方法中又add(new Date())写入Date类型,就会导致类型混乱,因此无法处理,但是null除外,它是任何引用类型的实例。

说白了,Java的泛型系统是类型安全优先的,不确定类型的泛型可读不可写。

类型通配符还能进行类型范围的限制,例如如果不希望List<?>可以传入任意一种类型,只希望传入某一类具体的类型,在设置类型通配符时,可以添加extendssuper限制条件,叫做受限制的通配符,extends代表某种类型及子类,super代表某种类型及父类。

例如有这样的一些类:

/** * 动物 */public class Animal {}/** * 猫 */public class Cat extends Animal {}/** * 狗 */public class Dog extends Animal {}/** * 英短猫 */public class YingDuan extends Cat {    }/** * 布偶猫 */public class BuOu extends Cat {    }

首先看extends,extends设置的是类型的上限,保证传入的类型不能超过某个类型,在使用时,如果只希望泛型参数类型是某个类型及其子类,List的类型参数就可以写成? extends,例如:List<? extends Animal>就是只允许传入的泛型类型是Animal及其子类,这样修饰的泛型可读,读出为父类Animal类型,但不可写

为什么不能写入?因为允许写入会导致类型混乱,只要泛型中限制某个类及其子类,那随着类的不断继承就一定会出现更小的子类,当更小的子类作为类型参数时,比这个子类大一些的父类祖父类对象就不能写到泛型修饰的变量中,因为没有子类引用指向父类的道理。再者假如两个兄弟类AB继承自同一父类,AB甚至没有父子关系,当A类作为类型形参,B类的对象更不能写到A类的泛型中。

例如传进来的list是个List<Dog>,方法中又去add(new Cat())会导致类型混乱,因为虽然Cat和Dog都继承自Animal但是Cat不是Dog的子类,会破坏List<Dog>的类型一致性。再例如传进来的是个List<Dog>,方法中又去add(new Animal())也会导致类型混乱,所以extends修饰的泛型禁止写入任何一个非null实例

public class Demo {    public static void test(List<? extends Animal> list) {        Animal animal = list.get(1); //获取返回值时,由父类Animal接收        list.add(new Cat()); //编译出错❌                list.add(new YingDuan()); //编译出错❌                list.add(new Dog()); //编译出错❌        list.add(new Animal()); //同样编译出错❌    }}

再来看super,super和extends的情况会略有不同,super代表限制类型为某种类型及父类,设置的是类型的下限,保证传入的泛型类型不能低于某个类型,super修饰的泛型可读,但只能读出为Object,可写,但只能写入对应类型及其子类,例如? super Cat修饰的变量只允许赋值Cat类及其子类的对象,因为Cat类及其子类的对象肯定可以被Cat类及其父类的引用指向

public class Demo{    public static void test(List<? super Cat> list) {        Object object = list.get(1);        list.add(new Animal()); //编译出错❌        list.add(new Cat()); //编译通过✅        list.add(new YingDuan()); //编译通过✅        list.add(new BuOu()); //编译通过✅        list.add(null); //编译通过✅        list.add(new Dog()); //编译出错❌    }}

举例来讲的话,可以传给List<? super Cat> list的不是“List<猫>”就是“List<动物>”,所以首先不能add“动物”进去,因为有可能传进来的是“List<猫>”,只有“动物”包含“猫”,没有“猫”包含“动物”。同理,不能add“狗”进去,因为“狗”属于“动物”但不属于“猫”(废话)。所以只有“猫”和“英短”以及“布偶”能add进去,因为无论传进来的是“List<猫>”还是“List<动物>”,“英短”和“布偶”既直接继承自“猫”,也间接继承自“动物”,而“猫”本身就能添加进去

所以只有类及其子类(猫、英短、布偶)可写入super修饰的泛型是因为这样符合程序里面类的继承关系,不会导致泛型中类型混乱。说白了就是:存放“动物”的List,存一只“猫”进去也行,存放“猫”的List,存进去“英短”以及“布偶”逻辑上都是正确的,都可以实现父类引用指向子类对象而不是颠倒过来。

上面例子中,List是一个带有泛型,但是泛型参数没有类型限制的类,如果定义一个泛型类,并限制类型参数的范围,该怎样和类型通配符搭配使用呢

此处定义一个限制类型参数的泛型类Pet,指定类型上限是Animal

public class Pet<T extends Animal> {    private T thing;    public T get() {        return thing;    }    public void set(T t) {        thing = t;    }}

使用类型通配符?时,类型参数需严格按照定义泛型时指定的上限,除了读取时返回的都是上限Animal类型,其他的和不加类型限制的泛型类没有区别,都是可读不可写,extends和super修饰的类型通配符也类似,可直接看结论:

public static void main(String[] args) {    Pet<?> pet = new Pet<Object>(); //编译出错❌    Pet<?> pet2 = new Pet<>();    pet2.set(new Animal()); //编译出错❌    Animal animal = pet2.get();}
public static void main(String[] args) {    Pet<? extends Cat> pet = new Pet<YingDuan>();    pet.set(new Cat()); //编译出错❌    Cat cat = pet.get();}
public static void main(String[] args) {    Pet<? super Cat> pet1 = new Pet<Animal>();        Pet<? super Dog> pet2 = new Pet<>();    Pet<? super Cat> pet3 = new Pet<Object>(); //编译出错❌    Animal animal1 = pet1.get();    Animal animal2 = pet2.get();}

💡可直接记住结论:
? extends T → “只能读(读出是T或类型上限)不能写”
? super T → “能写(T和T的子类),读出来只能是Object或者类型上限”

类型通配符和类型参数的区别:

用法位置意义是否允许
? extends Cat通配符接受某类或子类(只读)✅ 允许
? super Cat通配符接受某类或父类(只写)✅ 允许
T extends Cat类型参数限制上界,T至少是某类✅ 允许
T super Cat类型参数企图限制下界(但 Java 不支持)❌ 不允许

Java开篇

2021年12月29日 00:00

栏目持续更新中

一、引言

Java基础是全站的开篇,但是这个系列只会整理Java基础类库的使用和最新特性以及一些底层原理和源码解读,不会赘述JDK的安装配置和面向对象等最基础的内容。

二、Java版本发展

  • 1990年末, Sun公司成立了一个由James Gosling领导的”Green 计划”,准备为下一代智能家电 (如电视机、微波炉、电话)编写一个通用控制系统,在尝试了使用C++和改造C++未果后,决定创造一种全新的语言: Oak
  • 1992年夏,Green计划己经完成了新平台的部分功能,包括Green操作系统、Oak的程序设计语言、类库等
  • 1994年,互联网和浏览器的出现,开发组意识到Oak非常适合于互联网,对Oak进行了小规模的改造运行在浏览器,并更名为Java
  • 1995年初,Sun推出了Java语言
  • 1996年初,发布JDK1.0,这个版本包括两部分: 运行环境(JRE)和开发环境(JDK)
  • 1997年2月,发布JDK1.1,增加了JIT(即时编译)编译器
  • 1998年12月,Sun发布了Java历史上最重要的JDK版本:JDK1.2,伴随JDK1.2一同发布的还有JSP/Servlet、EJB等规范,并将Java分成了J2EE、J2SE和J2ME三个版本
  • 2002年2月,Sun发布了JDK历史上最为成熟的版本: JDK1.4
  • 2004年10月,发布里程式板本:JDK1.5,为突出此版本的重要性,更名为JDK5.0(Java5),同时,Sun将J2EE、J2ME也相应地改名为Java EE和Java ME,增加了诸如泛型、增强的for语句、可变数量的形参、注释 (Annotations)、自动拆箱和装箱等功能
  • 2006年12月,Sun公司发布了JDK1.6(Java6)
  • 2009年4月20日,Oracle宣布将以每股9.5美元的价格收购Sun
  • 2011年,发布JDK1.7(Java7),是Oracle来发布的第一个Java版本,引入了二进制整数、支持字符串的switch语句、菱形语法、多异常捕捉、自动关闭资源的try语句等新特性。
  • 2014年,发布Java8,是继Java5以来变化最大的版本,带来了全新的Lambda表达式、流式编程等大量新特性,具体见:Java8的新特性
  • 2017年9月,发布Java9,提供了超过150项新特性,主要包括模块化系统,jshell交互工具,jdk编译工具,java公共API以及安全增强,而且采用了更高效、更智能的G1垃圾回收器,完全对Java体系进行了改变,让庞大的Java语言更轻量化。从Java9开始,Java版本更迭从特性驱动改为时间驱动,每个版本之间的更迭周期调整为6个月,但是LTS版本的更迭时间为3年。同时将Oracle JDK的原商业特性进行开源。
  • 2018年3月,发布Java10
  • 2018年9月,发布Java11

三、Java核心类库

3.1 JDK基础类库

待续

3.2 数据结构 (Collection,Map,Set)

待续

3.3 输入输出 (IO/NIO)

在Java中,和IO有关的操作封装在java.iojava.nio中,传统IO(java.io)是以流的形式实现输入输出操作的,还有基于通道和缓冲区的NIO(java.nio

I/O操作,有阻塞和非阻塞之分:

  • 阻塞:发起读取数据的线程是被阻塞的
  • 非阻塞:发起读取数据的线程不被阻塞,直接返回

也有同步和异步之分:

  • 同步:数据读取完成后,直接在接收到数据的线程上,紧接着进行拷贝操作
  • 异步:数据读取完成后,通过一个回调函数,在新的线程处理数据的拷贝

阻塞非阻塞和同步异步,描述的阶段和描述的事情是不同的,因此可以自由组合,Java语言将其组合为分为BIO,NIO,和AIO三种不同的IO,与Linux的IO模型(详见:浅谈Linux(Unix)的I/O模型)对应的话,BIO对应的是Blocking I/O

  • BIO

    同步的,阻塞的IO,位于java.io包下,是Java的传统IO,基于流,IO流的分类方式有很多,按照方向分为输入流和输出流,按照数据传输单位又能分为字节流和字符流

  • NIO

    同步的,非阻塞的IO,位于java.nio包下

  • AIO

    异步的,非阻塞的IO,也位于java.nio包下

3.4 网络 (Socket)

3.5 线程 (Thread)

本部分主要讲述了使用Java语言来实现多线程的程序,包括线程的创建方式,线程基本方法的使用,多线程操作共享数据导致的安全问题以及死锁现象的原因和避免死锁,多线程运行过程中的协作和通信机制以及调用各种线程的方法时线程状态和生命周期的改变等。

序号文章名概述
1Java的线程和常见方法进程和线程,并发和并行,线程的创建和常见方法
2Java线程安全和同步机制多线程导致的问题,线程的同步机制,死锁
3Java线程间的通信机制线程的通信,等待唤醒机制wait/notify
4Java线程的状态不同线程状态之间的转换过程
5volatile作用分析volatile关键字的作用,可见性、原子性和重排序

3.6 并发编程 (JUC)

JUC就是Java在并发编程中使用的工具包java.util.concurrent的简称,包括java.util.concurrentjava.util.concurrent.atomicjava.util.concurrent.locks等,起始于JDK1.5,是Java语言中增强版的多线程和高并发工具,拥有更加强大的多线程实现,本章内容会介绍些JUC中常用的并发编程工具类及其实现原理,需要在理解了第3.5小节的线程一章的基础上学习

主要涉及:

  • 锁,可重入锁,公平/非公平锁,读写锁相关:java.util.concurrent.locks.Lockjava.util.concurrent.locks.Conditionjava.util.concurrent.locks.ReadWriteLock
  • 异步计算 java.util.concurrent.CompletableFuturejava.util.concurrent.FutureTaskjava.util.concurrent.Callable
  • 阻塞队列 java.util.concurrent.BlockingQueue
  • 线程池 java.util.concurrent.ExecutorService
  • 任务拆分合并工具 java.util.concurrent.ForkJoinPooljava.util.concurrent.ForkJoinTask
  • 线程安全容器 java.util.concurrent.CopyOnWriteArrayListjava.util.concurrent.ConcurrentHashMapjava.util.concurrent.CopyOnWriteArraySet
  • 并发控制工具 java.util.concurrent.CountDownLatchjava.util.concurrent.CyclicBarrierjava.util.concurrent.Semaphore
  • 各种原子类,例如java.util.concurrent.atomic.AtomicIntegerjava.util.concurrent.atomic.AtomicReference
  • 多线程编程的一些问题:缓存行对齐、锁的粗化,消除等
序号文章名概述
1JUC可重入锁ReentrantLockReentrantLock实现更精准的锁操作
2JUC读写锁ReadWriteLockReadWriteLock读写锁实现读写分离

3.7 反射 (Reflect)

待续

3.8 JDK其他工具和类

序号文章名概述
1Java实现LDAP登录使用Java与LDAP进行交互

四、Java的设计模式

序号文章名概述
1Java单例Java中单例模式的几种实现形式

五、参考

  1. 《疯狂Java讲义》,作者:李刚,电子工业出版社,2018年1月
  2. 《深入理解Java核心技术》,作者:张洪亮,电子工业出版社,2022年5月
  3. 《Effective Java》,作者:Joshua Bloch,机械工业出版社,2009年1月
❌