夯实Java基础系列13:深刻理解Java中的泛型

本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到个人仓库里查看html

https://github.com/h2pl/Java-Tutorialjava

喜欢的话麻烦点下Star、Fork、Watch三连哈,感谢你的支持。git

文章首发于个人我的博客:程序员

www.how2playlife.comgithub

本文是微信公众号【Java技术江湖】的《夯实Java基础系列博文》其中一篇,本文部份内容来源于网络,为了把本文主题讲得清晰透彻,也整合了不少我认为不错的技术博客内容,引用其中了一些比较好的博客文章,若有侵权,请联系做者。面试

该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每一个Java知识点背后的实现原理,更完整地了解整个Java技术体系,造成本身的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每一个知识点对应的面试题以及参考答案。算法

@[toc]
若是对本系列文章有什么建议,或者是有什么疑问的话,也能够关注公众号【Java技术江湖】联系做者,欢迎你参与本系列博文的创做和修订。 编程

<!-- more -->后端

泛型概述

泛型在java中有很重要的地位,在面向对象编程及各类设计模式中有很是普遍的应用。设计模式

什么是泛型?为何要使用泛型?

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,而后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,相似于方法中的变量参数,此时类型也定义成参数形式(能够称之为类型形参),而后在使用/调用时传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不建立新的类型的状况下,经过泛型指定的不一样类型来控制形参具体限制的类型)。也就是说在泛型使用过程当中,操做的数据类型被指定为一个参数,这种参数类型能够用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

一个栗子

一个被举了无数次的例子:

List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);

for(int i = 0; i< arrayList.size();i++){
    String item = (String)arrayList.get(i);
    Log.d("泛型测试","item = " + item);
}

毫无疑问,程序的运行结果会以崩溃结束:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

ArrayList能够存听任意类型,例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,所以程序崩溃了。为了解决相似这样的问题(在编译阶段就能够解决),泛型应运而生。

咱们将第一行声明初始化list的代码更改一下,编译器会在编译阶段就可以帮咱们发现相似这样的问题。

List<String> arrayList = new ArrayList<String>();
...
//arrayList.add(100); 在编译阶段,编译器就会报错

特性

泛型只在编译阶段有效。看下面的代码:

List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();

Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();

if(classStringArrayList.equals(classIntegerArrayList)){
    Log.d("泛型测试","类型相同");
}
经过上面的例子能够证实,在编译以后程序会采起去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程当中,正确检验泛型结果后,会将泛型的相关信息擦出,而且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

对此总结成一句话:泛型类型在逻辑上看以当作是多个不一样的类型,实际上都是相同的基本类型。

泛型的使用方式

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

泛型类

泛型类型用于类的定义中,被称为泛型类。经过泛型能够完成对一组类的操做对外开放相同的接口。最典型的就是各类容器类,如:List、Set、Map。

泛型类的最基本写法(这么看可能会有点晕,会在下面的例子中详解):

class 类名称 <泛型标识:能够随便写任意标识号,标识指定的泛型的类型>{
  private 泛型标识 /*(成员变量类型)*/ var; 
  .....

  }

一个最普通的泛型类:

//此处T能够随便写为任意标识,常见的如T、E、K、V等形式的参数经常使用于表示泛型

//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
    //在类中声明的泛型整个类里面均可以用,除了静态部分,由于泛型是实例化时声明的。
    //静态区域的代码在编译时就已经肯定,只与类相关
    class A <E>{
        T t;
    }
    //类里面的方法或类中再次声明同名泛型是容许的,而且该泛型会覆盖掉父类的同名泛型T
    class B <T>{
        T t;
    }
    //静态内部类也可使用泛型,实例化时赋予泛型实际类型
    static class C <T> {
        T t;
    }
    public static void main(String[] args) {
        //报错,不能使用T泛型,由于泛型T属于实例不属于类
//        T t = null;
    }

    //key这个成员变量的类型为T,T的类型由外部指定
    private T key;

    public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }

    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}
12-27 09:20:04.432 13063-13063/? D/泛型测试: key is 123456

12-27 09:20:04.432 13063-13063/? D/泛型测试: key is key_vlaue

定义的泛型类,就必定要传入泛型类型实参么?并非这样,在使用泛型的时候若是传入泛型实参,则会根据传入的泛型实参作相应的限制,此时泛型才会起到本应起到的限制做用。若是不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型能够为任何的类型。

看一个例子:

Generic generic = new Generic("111111");
Generic generic1 = new Generic(4444);
Generic generic2 = new Generic(55.55);
Generic generic3 = new Generic(false);

Log.d("泛型测试","key is " + generic.getKey());
Log.d("泛型测试","key is " + generic1.getKey());
Log.d("泛型测试","key is " + generic2.getKey());
Log.d("泛型测试","key is " + generic3.getKey());

D/泛型测试: key is 111111
D/泛型测试: key is 4444
D/泛型测试: key is 55.55
D/泛型测试: key is false

注意:
泛型的类型参数只能是类类型,不能是简单类型。
不能对确切的泛型类型使用instanceof操做。以下面的操做是非法的,编译时会出错。

if(ex_num instanceof Generic<Number>){   
    }

泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各类类的生产器中,能够看一个例子:

//定义一个泛型接口
public interface Generator<T> {
    public T next();
}

当实现泛型接口的类,未传入泛型实参时:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一块儿加到类中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 若是不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

当实现泛型接口的类,传入泛型实参时:

/**
 * 传入泛型实参时:
 * 定义一个生产器实现这个接口,虽然咱们只建立了一个泛型接口Generator<T>
 * 可是咱们能够为T传入无数个实参,造成无数种类型的Generator接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则全部使用泛型的地方都要替换成传入的实参类型
 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
 */
public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

泛型通配符

咱们知道Ingeter是Number的一个子类,同时在特性章节中咱们也验证过Generic<Ingeter>与Generic<Number>其实是相同的一种基本类型。那么问题来了,在使用Generic<Number>做为形参的方法中,可否使用Generic<Ingeter>的实例传入呢?在逻辑上相似于Generic<Number>和Generic<Ingeter>是否能够当作具备父子关系的泛型类型呢?

为了弄清楚这个问题,咱们使用Generic<T>这个泛型类继续看下面的例子:

public void showKeyValue1(Generic<Number> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}

Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);

showKeyValue(gNumber);

// showKeyValue这个方法编译器会为咱们报错:Generic<java.lang.Integer> 
// cannot be applied to Generic<java.lang.Number>
// showKeyValue(gInteger);

经过提示信息咱们能够看到Generic<Integer>不能被看做为`Generic<Number>的子类。由此能够看出:同一种泛型能够对应多个版本(由于参数类型是不肯定的),不一样版本的泛型类实例是不兼容的。

回到上面的例子,如何解决上面的问题?总不能为了定义一个新的方法来处理Generic<Integer>类型的类,这显然与java中的多台理念相违背。所以咱们须要一个在逻辑上能够表示同时是Generic<Integer>和Generic<Number>父类的引用类型。由此类型通配符应运而生。

咱们能够将上面的方法改一下:

public void showKeyValue1(Generic<?> obj){
    Log.d("泛型测试","key value is " + obj.getKey());

类型通配符通常是使用?代替具体的类型实参,注意, 此处的?和Number、String、Integer同样都是一种实际的类型,能够把?当作全部类型的父类。是一种真实的类型。

能够解决当具体类型不肯定的时候,这个通配符就是 ? ;当操做类型时,不须要使用类型的具体功能时,只使用Object类中的功能。那么能够用 ? 通配符来表未知类型

public void showKeyValue(Generic<Number> obj){

System.out.println(obj);
}
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);

public void test () {
//        showKeyValue(gInteger);该方法会报错
    showKeyValue1(gInteger);
}

public void showKeyValue1(Generic<?> obj) {
    System.out.println(obj);
}
// showKeyValue这个方法编译器会为咱们报错:Generic<java.lang.Integer>
// cannot be applied to Generic<java.lang.Number>
// showKeyValue(gInteger);

泛型方法

在java中,泛型类的定义很是简单,可是泛型方法就比较复杂了。

尤为是咱们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法,这样在初学者中很是容易将泛型方法理解错了。
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。

/**
 * 泛型方法的基本介绍
 * @param tClass 传入的泛型实参
 * @return T 返回值为T类型
 * 说明:
 *     1)public 与 返回值中间<T>很是重要,能够理解为声明此方法为泛型方法。
 *     2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并非泛型方法。
 *     3)<T>代表该方法将使用泛型类型T,此时才能够在方法中使用泛型类型T。
 *     4)与泛型类的定义同样,此处T能够随便写为任意标识,常见的如T、E、K、V等形式的参数经常使用于表示泛型。
 */
    public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
      IllegalAccessException{
            T instance = tClass.newInstance();
            return instance;
    }

Object obj = genericMethod(Class.forName("com.test.test"));

泛型方法的基本用法

光看上面的例子有的同窗可能依然会很是迷糊,咱们再经过一个例子,把我泛型方法再总结一下。

/** 
 * 这才是一个真正的泛型方法。
 * 首先在public与返回值之间的<T>必不可少,这代表这是一个泛型方法,而且声明了一个泛型T
 * 这个T能够出如今这个泛型方法的任意位置.
 * 泛型的数量也能够为任意多个 
 *    如:public <T,K> K showKeyName(Generic<T> container){
 *        ...
 *        }
 */

    public class 泛型方法 {
    @Test
    public void test() {
        test1();
        test2(new Integer(2));
        test3(new int[3],new Object());

        //打印结果
//        null
//        2
//        [I@3d8c7aca
//        java.lang.Object@5ebec15
    }
    //该方法使用泛型T
    public <T> void test1() {
        T t = null;
        System.out.println(t);
    }
    //该方法使用泛型T
    //而且参数和返回值都是T类型
    public <T> T test2(T t) {
        System.out.println(t);
        return t;
    }

    //该方法使用泛型T,E
    //参数包括T,E
    public <T, E> void test3(T t, E e) {
        System.out.println(t);
        System.out.println(e);
    }
}

类中的泛型方法

固然这并非泛型方法的所有,泛型方法能够出现杂任何地方和任何场景中使用。可是有一种状况是很是特殊的,当泛型方法出如今泛型类中时,咱们再经过一个例子看一下

//注意泛型类先写类名再写泛型,泛型方法先写泛型再写方法名
//类中声明的泛型在成员和方法中可用
class A <T, E>{
    {
        T t1 ;
    }
    A (T t){
        this.t = t;
    }
    T t;

    public void test1() {
        System.out.println(this.t);
    }

    public void test2(T t,E e) {
        System.out.println(t);
        System.out.println(e);
    }
}
@Test
public void run () {
    A <Integer,String > a = new A<>(1);
    a.test1();
    a.test2(2,"ds");
//        1
//        2
//        ds
}

static class B <T>{
    T t;
    public void go () {
        System.out.println(t);
    }
}

泛型方法与可变参数

再看一个泛型方法和可变参数的例子:

public class 泛型和可变参数 {
    @Test
    public void test () {
        printMsg("dasd",1,"dasd",2.0,false);
        print("dasdas","dasdas", "aa");
    }
    //普通可变参数只能适配一种类型
    public void print(String ... args) {
        for(String t : args){
            System.out.println(t);
        }
    }
    //泛型的可变参数能够匹配全部类型的参数。。有点无敌
    public <T> void printMsg( T... args){
        for(T t : args){
            System.out.println(t);
        }
    }
        //打印结果:
    //dasd
    //1
    //dasd
    //2.0
    //false

}

静态方法与泛型

静态方法有一种状况须要注意一下,那就是在类中的静态方法使用泛型:静态方法没法访问类上定义的泛型;若是静态方法操做的引用数据类型不肯定的时候,必需要将泛型定义在方法上。

即:若是静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。

public class StaticGenerator<T> {
    ....
    ....
    /**
     * 若是在类中定义使用泛型的静态方法,须要添加额外的泛型声明(将这个方法定义成泛型方法)
     * 即便静态方法要使用泛型类中已经声明过的泛型也不能够。
     * 如:public static void show(T t){..},此时编译器会提示错误信息:
          "StaticGenerator cannot be refrenced from static context"
     */
    public static <T> void show(T t){

    }
}

泛型方法总结

泛型方法能使方法独立于类而产生变化,如下是一个基本的指导原则:

不管什么时候,若是你能作到,你就该尽可能使用泛型方法。也就是说,若是使用泛型方法将整个类泛型化,那么就应该使用泛型方法。另外对于一个static的方法而已,没法访问泛型类型的参数。因此若是static方法要使用泛型能力,就必须使其成为泛型方法。

泛型上下边界

在使用泛型的时候,咱们还能够为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

为泛型添加上边界,即传入的类型实参必须是指定类型的子类型

public class 泛型通配符与边界 {
    public void showKeyValue(Generic<Number> obj){
        System.out.println("key value is " + obj.getKey());
    }
    @Test
    public void main() {
        Generic<Integer> gInteger = new Generic<Integer>(123);
        Generic<Number> gNumber = new Generic<Number>(456);
        showKeyValue(gNumber);
        //泛型中的子类也没法做为父类引用传入
//        showKeyValue(gInteger);
    }
    //直接使用?通配符能够接受任何类型做为泛型传入
    public void showKeyValueYeah(Generic<?> obj) {
        System.out.println(obj);
    }
    //只能传入number的子类或者number
    public void showKeyValue1(Generic<? extends Number> obj){
        System.out.println(obj);
    }

    //只能传入Integer的父类或者Integer
    public void showKeyValue2(Generic<? super Integer> obj){
        System.out.println(obj);
    }

    @Test
    public void testup () {
        //这一行代码编译器会提示错误,由于String类型并非Number类型的子类
        //showKeyValue1(generic1);
        Generic<String> generic1 = new Generic<String>("11111");
        Generic<Integer> generic2 = new Generic<Integer>(2222);
        Generic<Float> generic3 = new Generic<Float>(2.4f);
        Generic<Double> generic4 = new Generic<Double>(2.56);

        showKeyValue1(generic2);
        showKeyValue1(generic3);
        showKeyValue1(generic4);
    }

    @Test
    public void testdown () {

        Generic<String> generic1 = new Generic<String>("11111");
        Generic<Integer> generic2 = new Generic<Integer>(2222);
        Generic<Number> generic3 = new Generic<Number>(2);
//        showKeyValue2(generic1);本行报错,由于String并非Integer的父类
        showKeyValue2(generic2);
        showKeyValue2(generic3);
    }
}

== 关于泛型数组要提一下 ==

看到了不少文章中都会提起泛型数组,通过查看sun的说明文档,在java中是”不能建立一个确切的泛型类型的数组”的。

也就是说下面的这个例子是不能够的:

List<String>[] ls = new ArrayList<String>[10];  

而使用通配符建立泛型数组是能够的,以下面这个例子:

List<?>[] ls = new ArrayList<?>[10];  

这样也是能够的:

List<String>[] ls = new ArrayList[10];

下面使用Sun的一篇文档的一个例子来讲明这个问题:

List<String>[] lsa = new List<String>[10]; // Not really allowed.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; // Unsound, but passes run time store check    
String s = lsa[1].get(0); // Run-time error: ClassCastException.
这种状况下,因为JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,因此能够给oa[1]赋上一个ArrayList而不会出现异常,可是在取出数据的时候却要作一次类型转换,因此就会出现ClassCastException,若是能够进行泛型数组的声明,上面说的这种状况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。

而对泛型数组的声明进行限制,对于这样的状况,能够在编译期提示代码有类型安全问题,比没有任何提示要强不少。
下面采用通配符的方式是被容许的:数组的类型不能够是类型变量,除非是采用通配符的方式,由于对于通配符的方式,最后取出数据是要作显式的类型转换的。

List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; // Correct.    
Integer i = (Integer) lsa[1].get(0); // OK

最后

本文中的例子主要是为了阐述泛型中的一些思想而简单举出的,并不必定有着实际的可用性。另外,一提到泛型,相信你们用到最多的就是在集合中,其实,在实际的编程过程当中,本身可使用泛型去简化开发,且能很好的保证代码质量。

泛型常见面试题

  1. Java中的泛型是什么 ? 使用泛型的好处是什么?

这是在各类Java泛型面试中,一开场你就会被问到的问题中的一个,主要集中在初级和中级面试中。那些拥有Java1.4或更早版本的开发背景的人 都知道,在集合中存储对象并在使用前进行类型转换是多么的不方便。泛型防止了那种状况的发生。它提供了编译期的类型安全,确保你只能把正确类型的对象放入 集合中,避免了在运行时出现ClassCastException。

  1. Java的泛型是如何工做的 ? 什么是类型擦除 ?

这是一道更好的泛型面试题。泛型是经过类型擦除来实现的,编译器在编译时擦除了全部类型相关的信息,因此在运行时不存在任何类型相关的信息。例如 List<String>在运行时仅用一个List来表示。这样作的目的,是确保能和Java 5以前的版本开发二进制类库进行兼容。你没法在运行时访问到类型参数,由于编译器已经把泛型类型转换成了原始类型。根据你对这个泛型问题的回答状况,你会 获得一些后续提问,好比为何泛型是由类型擦除来实现的或者给你展现一些会致使编译器出错的错误泛型代码。请阅读个人Java中泛型是如何工做的来了解更 多信息。

  1. 什么是泛型中的限定通配符和非限定通配符 ?

这是另外一个很是流行的Java泛型面试题。限定通配符对类型进行了限制。有两种限定通配符,一种是<? extends T>它经过确保类型必须是T的子类来设定类型的上界,另外一种是<? super T>它经过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,不然会致使编译错误。另外一方面<?>表 示了非限定通配符,由于<?>能够用任意类型来替代。更多信息请参阅个人文章泛型中限定通配符和非限定通配符之间的区别。

  1. List<? extends T>和List <? super T>之间有什么区别 ?

这和上一个面试题有联系,有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符。这两个List的声明都是 限定通配符的例子,List<? extends T>能够接受任何继承自T的类型的List,而List<? super T>能够接受任何T的父类构成的List。例如List<? extends Number>能够接受List<Integer>或List<Float>。在本段出现的链接中能够找到更多信息。

  1. 如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型?

编写泛型方法并不困难,你须要用泛型类型来替代原始类型,好比使用T, E or K,V等被普遍承认的类型占位符。泛型方法的例子请参阅Java集合类框架。最简单的状况下,一个泛型方法可能会像这样:

public V put(K key, V value) {

return cache.put(key, value);

}

  1. Java中如何使用泛型编写带有参数的类?

这是上一道面试题的延伸。面试官可能会要求你用泛型编写一个类型安全的类,而不是编写一个泛型方法。关键仍然是使用泛型类型来代替原始类型,并且要使用JDK中采用的标准占位符。

  1. 编写一段泛型程序来实现LRU缓存?

对于喜欢Java编程的人来讲这至关因而一次练习。给你个提示,LinkedHashMap能够用来实现固定大小的LRU缓存,当LRU缓存已经满 了的时候,它会把最老的键值对移出缓存。LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put() 和putAll()调用来删除最老的键值对。固然,若是你已经编写了一个可运行的JUnit测试,你也能够随意编写你本身的实现代码。

  1. 你能够把List<String>传递给一个接受List<Object>参数的方法吗?

对任何一个不太熟悉泛型的人来讲,这个Java泛型题目看起来使人疑惑,由于乍看起来String是一种Object,因此 List<String>应当能够用在须要List<Object>的地方,可是事实并不是如此。真这样作的话会致使编译错误。如 果你再深一步考虑,你会发现Java这样作是有意义的,由于List<Object>能够存储任何类型的对象包括String, Integer等等,而List<String>却只能用来存储Strings。

List<Object> objectList;

List<String> stringList;

objectList = stringList; //compilation error incompatible types

  1. Array中能够用泛型吗?

这多是Java泛型面试题中最简单的一个了,固然前提是你要知道Array事实上并不支持泛型,这也是为何Joshua Bloch在Effective Java一书中建议使用List来代替Array,由于List能够提供编译期的类型安全保证,而Array却不能。

  1. 如何阻止Java中的类型未检查的警告?

若是你把泛型和原始类型混合起来使用,例以下列代码,Java 5的javac编译器会产生类型未检查的警告,例如

List<String> rawList = new ArrayList()

注意: Hello.java使用了未检查或称为不安全的操做;

这种警告可使用@SuppressWarnings(“unchecked”)注解来屏蔽。

参考文章

https://www.cnblogs.com/huaji...
https://www.cnblogs.com/jpfss...
https://www.cnblogs.com/dengc...
https://www.cnblogs.com/cat52...
https://www.cnblogs.com/copri...

微信公众号

Java技术江湖

若是你们想要实时关注我更新的文章以及分享的干货的话,能够关注个人公众号【Java技术江湖】一位阿里 Java 工程师的技术小站,做者黄小斜,专一 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!

Java工程师必备学习资源: 一些Java工程师经常使用学习资源,关注公众号后,后台回复关键字 “Java” 便可免费无套路获取。

个人公众号

我的公众号:黄小斜

做者是 985 硕士,蚂蚁金服 JAVA 工程师,专一于 JAVA 后端技术栈:SpringBoot、MySQL、分布式、中间件、微服务,同时也懂点投资理财,偶尔讲点算法和计算机理论基础,坚持学习和写做,相信终身学习的力量!

程序员3T技术学习资源: 一些程序员学习技术的资源大礼包,关注公众号后,后台回复关键字 “资料” 便可免费无套路获取。

相关文章
相关标签/搜索