前言

泛型是Java基础知识的重点,虽然我们在初学Java的时候,都学过泛型,觉得自己掌握对于Java泛型的使用(全是错觉),往后的日子,当我们深入去阅读一些框架源码,你就发现了,自己会的只是简单的使用,却看不懂别人的泛型代码是怎么写的,还可以这样,没错,别人写出来的代码那叫艺术,而我......

探讨

Java语言为什么存在着泛型,而像一些动态语言Python,JavaScipt却没有泛型的概念?

原因是,像JavaC#这样的静态编译型的语言,它们在传递参数的时候,参数的类型,必须是明确的,看一个例子,简单编写一个存放int类型的栈—StackInt,代码如下:

public class StackInt {

    private int maxSize;
private int[] items;
private int top; public StackInt(int maxSize){
this.maxSize = maxSize;
this.items = new int[maxSize];
this.top = -1;
} public boolean isFull(){
return this.top == this.maxSize-1;
} public boolean isNull(){
return this.top <= -1;
} public boolean push(int value){
if(this.isFull()){
return false;
}
this.items[++this.top] = value;
return true;
} public int pop(){
if(this.isNull()){
throw new RuntimeException("当前栈中无数据");
}
int value = this.items[top];
--top;
return value;
}
}

在这里使用构造函数初始化一个StackInt对象时,可以传入String字符串吗?很明显是不行的,我们要求的是int类型,传入字符串String类型,这样在语法检查阶段时会报错的,像Java这样的静态编译型的语言,参数的类型要求是明确的

泛型解决了什么问题?

参数不安全:引入泛型,能够在编译阶段找出代码的问题,而不是在运行阶段

泛型要求在声明时指定实际数据类型,Java 编译器在编译时会对泛型代码做强类型检查,并在代码违反类型安全时发出告警。早发现,早治理,把隐患扼杀于摇篮,在编译时发现并修复错误所付出的代价远比在运行时小。

避免类型转换:

未使用泛型:

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); //需要在取出Value的时候进行强制转换

使用泛型:

List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); //不需要强制转换

重复编码::通过使用泛型,可以实现通用编码,可以处理不同类型的集合,并且类型安全且易于阅读。像上面的StackInt类,我们不能针对每个类型去编写对应类型的栈,那样太麻烦了,而泛型的出现就很好的解决了这点

扩展

在上面的StackInt类有一些不好的地方,那就是太具体了,不够抽象,不够抽象,那么它的复用性也是不高的,例如,在另外的场景下,我需要的是往栈里存String类型的字符串,或者是其他类型,那么StackInt类就做不到了,那么有什么方法能够做到呢?再写一个StackString类,不可能,那样不得累死。那就只有引入基类Object了,我们改进一下代码:

public class StackObject {

    private int maxSize;
private Object[] items;
private int top; public StackObject(int maxSize){
this.maxSize = maxSize;
this.items = new Object[maxSize];
this.top = -1;
} public boolean isFull(){
return this.top == this.maxSize-1;
} public boolean isNull(){
return this.top <= -1;
} public boolean push(Object value){
if(this.isFull()){
return false;
}
this.items[++this.top] = value;
return true;
} public Object pop(){
if(this.isNull()){
throw new RuntimeException("当前栈中无数据");
}
Object value = this.items[top];
--top;
return value;
}
}

使用StackObject可以存储任意类型的数据,那么这样做,又有什么优点和缺点呢?

优点:StackObject类变得相对抽象了,我们可以往里面存储任何类型的数据,这样就避免了写一些重复代码

缺点:

1、用Object表示的对象是比较抽象的,它失去了类型的特点,那么我们在做一些运算的时候,可能会频繁的拆箱装箱的过程

看上面的例图,我们理解的认为存放了两个数值,1234554321,将两个进行相加,这是很常见的操作,但是报错了,编译器给我们的提示是,+操作运算不能用于两个Object类型,那么只能对其进行类型转换,这也是我们上面说到的泛型能解决的问题,我们需要这样做,int sum = (int)val1 + (int)val2;,同时在涉及拆箱装箱时,是有一定性能的损耗的,关于拆箱装箱在这里不作描述,可以参考我写过的随笔—— 深入理解Java之装箱与拆箱

2、对于我们push进去的值,我们在取出的时候,容易忘记类型转换,或者不记得它的类型,类型转换错误,这在后面的一些业务可能埋下祸根,例如下面这个场景:直到运行时错误才暴露出来,这是不安全的,也是违反软件开发原则的,应该尽早的在编译阶段就发现问题,解决问题

3、使用Object太过于模糊了,没有具体类型的意义

最好不要用到Object,因为Object是一切类型的基类,也就是说他把一些类型的特点给抹除了,比如上面存的数字,对于数字来说,加法运算就是它的一个特点,但是用了Object,它就失去了这一特点,失去类型特有的行为

引入泛型

什么是泛型?

泛型:是被参数化的类或接口,是对类型的约定

泛型类

class name<T1, T2, ..., Tn> { /* ... */ }

一般将泛型中的类名称为原型,而将 <> 指定的参数称为类型参数<> 相当于类型的约定,T就是类型,相当于一个占位符,由我们在调用时指定

使用泛型改进一下上面StackObject类,但是,数组和泛型不能很好地结合。你不能实例化具有参数化类型的数组,例如下面的代码是不合格的:

public StackT(int maxSize){
this.maxSize = maxSize;
this.items = new T[maxSize];
this.top = -1;
}

Java 中不允许直接创建泛型数组,这是因为相比于C++,C#的语法,Java泛型其实是伪泛型,这点在后面会说到,但是,可以通过创建一个类型擦除的数组,然后转型的方式来创建泛型数组。

private int maxSize;
private T[] items;
private int top; public StackT(int maxSize){
this.maxSize = maxSize;
this.items = (T[]) new Object[maxSize];
this.top = -1;
}

实际上,真的需要存储泛型,还是使用容器更合适,回到原来的代码上,需要知道的是,泛型类型不能是基本类型的,需要是包装类

上面说到了Java 中不允许直接创建泛型数组,事实上,Java中的泛型我们是很难通new的方式去实例化对象,不仅仅是实例化对象,甚至是获取T的真实类型也是很难的,当然通过反射的机制还是可以获取到的,Java获取真实类型的方式有 3 种,分别是:

1、类名.class

2、对象.getClass

3、class.forName("全限定类名")

但是,在这里,12的方式都是做不到的,虽然我们在外边明确的传入了Integer类型,new StackT<Integer>(3);但是在StackT

类,使用T.class还是获取不到真实类型的,第 2 种方式的话,并没有传入对象,前面也说到是没有办法new方式实例化的,而通过反射机制是可以做到的,这里不作演示,需要了解的话可以参考 —— Java如何获得泛型类的真实类型Java通过反射获取泛型的类型

但是在C#中的泛型以及C++的模板,这是很容易做到的,所以说Java的泛型是伪泛型,Java并不是做不到像C#一样,而是为了迁就老的JDK语法所作出的妥协,至于上面为什么做不到这样,这就要说到泛型的类型擦除了。

再说类型擦除之前,先说一下泛型接口,和泛型方法吧

泛型接口

接口也可以声明泛型,泛型接口语法形式:

public interface Content<T> {
T text();
}

泛型接口有两种实现方式:

  • 实现接口的子类明确声明泛型类型
public class ContentImpl implements Content<Integer> {
private int text; public ContentImpl(int text) {
this.text = text;
} public static void main(String[] args) {
ContentImpl one = new ContentImpl(10);
System.out.print(one.text());
}
}
// Output:
// 10
  • 实现接口的子类不明确声明泛型类型
public class ContentImpl<T> implements Content<T> {
private T text; public ContentImpl(T text) {
this.text = text;
} @Override
public T text() { return text; } public static void main(String[] args) {
ContentImpl<String> two = new ContentImpl<>("ABC");
System.out.print(two.text());
}
}
// Output:
// ABC

泛型方法

泛型方法是引入其自己的类型参数的方法。泛型方法可以是普通方法、静态方法以及构造方法。

泛型方法语法形式如下:

public <T> T func(T obj) {}

是否拥有泛型方法,与其所在的类是否是泛型没有关系。

泛型方法的语法包括一个类型参数列表,在尖括号内,它出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际类型参数的占位符。

使用泛型方法的时候,通常不必指明类型参数,因为编译器会为我们找出具体的类型。这称为类型参数推断(type argument inference)。类型推断只对赋值操作有效,其他时候并不起作用。如果将一个泛型方法调用的结果作为参数,传递给另一个方法,这时编译器并不会执行推断。编译器会认为:调用泛型方法后,其返回值被赋给一个 Object 类型的变量。

public class GenericsMethod {
public static <T> void printClass(T obj) {
System.out.println(obj.getClass().toString());
} public static void main(String[] args) {
printClass("abc");
printClass(10);
}
}
// Output:
// class java.lang.String
// class java.lang.Integer

泛型方法中也可以使用可变参数列表

public class GenericVarargsMethod {
public static <T> List<T> makeList(T... args) {
List<T> result = new ArrayList<T>();
Collections.addAll(result, args);
return result;
} public static void main(String[] args) {
List<String> ls = makeList("A");
System.out.println(ls);
ls = makeList("A", "B", "C");
System.out.println(ls);
}
}
// Output:
// [A]
// [A, B, C]

类型擦除

事实上,Java的运行大致可以分为两个阶段,编译阶段运行阶段

那么对于Java泛型来说,当编译阶段过后,泛型 T 是已经被擦除了,所以在运行阶段,它已经丢失了 T 的具体信息,而我们去实例化一个对象的时候,比如T c = new T();,它的发生时机是在运行阶段,而在运行阶段,你要new T(),就需要知道 T 的具体类型,实际上这时候 T是被替换成Integer了,而JVM是不知道T的类型的,所以是没有办法实例化的。

那么,类型擦除做了什么呢?它做了以下工作:

  • 把泛型中的所有类型参数替换为 Object,如果指定类型边界,则使用类型边界来替换。因此,生成的字节码仅包含普通的类,接口和方法。
  • 擦除出现的类型声明,即去掉 <> 的内容。比如 T get() 方法声明就变成了 Object get()List<String> 就变成了 List。如有必要,插入类型转换以保持类型安全。
  • 生成桥接方法以保留扩展泛型类型中的多态性。类型擦除确保不为参数化类型创建新类;因此,泛型不会产生运行时开销。

让我们来看一个示例:

import java.util.*;

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);
} }
/* Output:
true
*/

ArrayList<String>ArrayList<Integer> 应该是不同的类型。不同的类型会有不同的行为。例如,如果尝试向 ArrayList<String> 中放入一个 Integer,所得到的行为(失败)和 向 ArrayList<Integer> 中放入一个 Integer 所得到的行为(成功)完全不同。但是结果输出的是true,这意味着使用泛型时,任何具体的类型信息都被擦除了,ArrayList<Object>ArrayList<Integer> 在运行时,JVM 将它们视为同一类型class java.util.ArrayList

再用一个例子来对于该谜题的补充:

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()));
} }
/* Output:
[E]
[K,V]
[Q]
[POSITION,MOMENTUM]
*/

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

残酷的现实是:在泛型代码内部,无法获取任何有关泛型参数类型的信息。

以上两个例子皆出《Java 编程思想》第五版 —— On Java 8中的例子,本文借助该例子,试图讲清楚Java泛型是使用类型擦除这里机制实现的,能力不足,有错误的地方,还请指正。关于On Java 8一书,已在github上开源,并有热心的伙伴将之翻译成中文,现在给出阅读地址,On Java 8

擦除的问题

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

考虑如下的代码段:

class Foo<T> {
T var;
}

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

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

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

继承问题

泛型时基于类型擦除实现的,所以,泛型类型无法向上转型

向上转型是指用子类实例去初始化父类,这是面向对象中多态的重要表现。

Integer 继承了 ObjectArrayList 继承了 List;但是 List<Interger> 却并非继承了 List<Object>

这是因为,泛型类并没有自己独有的 Class 类对象。比如:并不存在 List<Object>.class 或是 List<Interger>.class,Java 编译器会将二者都视为 List.class

如何解决上面所产生的问题:

其实并不一定要通过new的方式去实例化,我们可以通过显式的传入源类,一个Class<T> clazz的对象来补偿擦除,例如instanceof 操作,在程序中尝试使用 instanceof 将会失败。类型标签可以使用动态 isInstance() ,这样改进代码:

public class Improve<T> {

    //错误方法
public boolean f(Object arg) {
// error: illegal generic type for instanceof
if (arg instanceof T) {
return true;
}
return false;
}
//改进方法
Class<T> clazz; public Improve(Class<T> clazz) {
this.clazz = clazz;
} public boolean f(Object arg) {
return kind.isInstance(arg);
}
}

实例化:

试图在 new T() 是行不通的,部分原因是由于擦除,部分原因是编译器无法验证 T 是否具有默认(无参)构造函数。

Java 中的解决方案是传入一个工厂对象,并使用该对象创建新实例。方便的工厂对象只是 Class 对象,因此,如果使用类型标记,则可以使用 newInstance() 创建该类型的新对象:

class Improve<T> {
Class<T> kind; Improve(Class<T> kind) {
this.kind = kind;
} public T get(){
try {
return kind.newInstance();
} catch (InstantiationException |
IllegalAccessException e) {
throw new RuntimeException(e);
}
}
} class Employee {
@Override
public String toString() {
return "Employee";
}
} public class InstantiateGenericType {
public static void main(String[] args) {
Improve<Employee> fe = new Improve<>(Employee.class);
System.out.println(fe.get());
}
}
/* Output:
Employee
*/

通过这样改进代码,可以实现创建对象的实例,但是要注意的是,newInstance();方法调用无参构造函数的,如果传入的类型,没有无参构造的话,是会抛出InstantiationException异常的。

泛型数组

泛型数组这部分,我们在上面说到可以通过创建一个类型擦除的数组,然后转型的方式来创建泛型数组,这次我们可以通过显式的传入源类的方式来编写StackT类,解决创建泛型数组的问题,代码如下:

public class StackT<T> {

    private int maxSize;
private T[] items;
private int top; public StackT(int maxSize, Class<T> clazz){
this.maxSize = maxSize;
this.items = this.createArray(clazz);
this.top = -1;
} public boolean isFull(){
return this.top == this.maxSize-1;
} public boolean isNull(){
return this.top <= -1;
} public boolean push(T value){
if(this.isFull()){
return false;
}
this.items[++this.top] = value;
return true;
} public T pop(){
if(this.isNull()){
throw new RuntimeException("当前栈中无数据");
}
T value = this.items[top];
--top;
return value;
} private T[] createArray(Class<T> clazz){
T[] array =(T[])Array.newInstance(clazz, this.maxSize);
return array;
} }

边界

有时您可能希望限制可在参数化类型中用作类型参数的类型。类型边界可以对泛型的类型参数设置限制条件。例如,对数字进行操作的方法可能只想接受 Number 或其子类的实例。

要声明有界类型参数,请列出类型参数的名称,然后是 extends 关键字,后跟其限制类或接口。

类型边界的语法形式如下:

<T extends XXX>

示例:

public class GenericsExtendsDemo01 {
static <T extends Comparable<T>> T max(T x, T y, T z) {
T max = x; // 假设x是初始最大值
if (y.compareTo(max) > 0) {
max = y; //y 更大
}
if (z.compareTo(max) > 0) {
max = z; // 现在 z 更大
}
return max; // 返回最大对象
} public static void main(String[] args) {
System.out.println(max(3, 4, 5));
System.out.println(max(6.6, 8.8, 7.7));
System.out.println(max("pear", "apple", "orange"));
}
}
// Output:
// 5
// 8.8
// pear

示例说明:

上面的示例声明了一个泛型方法,类型参数 T extends Comparable<T> 表明传入方法中的类型必须实现了 Comparable 接口。

类型边界可以设置多个,语法形式如下:

<T extends B1 & B2 & B3>

注意:extends 关键字后面的第一个类型参数可以是类或接口,其他类型参数只能是接口。

通配符

通配符是Java泛型中的一个非常重要的知识点。很多时候,我们其实不是很理解通配符和泛型类型T区别,容易混淆在一起,其实还是很好理解的,T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 不行,比如如下这种 :

// 可以
T t = operate();
// 不可以
? car = operate();

但是这个并不是我们混淆的原因,虽然T 都表示不确定的类型,T 通常用于泛型类和泛型方法的定义,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。用代码解释一下,回到文章最初说的栈类StackT,我们以这个为基础来解释,上面的观点:

public class Why {
public static void main(String[] args) { StackT<Integer> stackT = new StackT<>(3, Integer.class);
stackT.push(8);
StackT<String> stackT1 = new StackT<>(3, String.class);
stackT1.push("7");
test(stackT1); }
public static void test(StackT stackT){
System.out.println(stackT.pop());
}
}
// Output: 8

以我们编写的StackT类,进行测试,编写一个test方法,传入参数类型StackT,上面的程序正常输出字符串"7" ,这没有什么问题,问题在这里失去了泛型的限定,传进去的实参StackT1,是被我们限定为StackT<String> ,但是我们通过编译器可以看到stackT.pop()出来的对象,并没有String类型的特有方法,也就是说,它其实是Object

那么我们就需要修改test方法的形参,改为:

public static void test(StackT<String> stackT){
System.out.println(stackT.pop());
}

这样子就回到了我们问题的本质来了,将形参修改为StackT<String>,这起到了泛型的限定作用,但是会出现这样的问题,如果我们需要向该方法传入StackT<Integer>类型的对象 stackT是,因为方法形参限定了StackT<String>,,这时候就报错了

这个时候就是通配符?起作用了,将方法形参改为StackT<?>就可以了,这也就确定了我们刚刚的结论,通配符通常是用于泛型传参,而不是泛型类的定义。

public static void test(StackT<?> stackT){
System.out.println(stackT.pop());
}

但是这种用法我们通常也不会去用,因为它还是失去了类型的特点,即当无界泛型通配符作为形参时,作为调用方,并不限定传递的实际参数类型。但是,在方法内部,泛型类的参数和返回值为泛型的方法,不能使用!

这里,StackT.push就不能用了,因为我并不知道?传的是Integer还是String ,还是其他类型,所以是会报错的。

但是我们有时候是有这样的需求的,我们在接收泛型栈StackT作为形参的时候,我想表达一种约束的关系,但是又不像StackT<String>一样,约束的比较死板,而Java是面向对象的语言,那么就会有继承的机制,我想要的约束关系是我能接收的泛型栈的类型都是Number类的派生类,即不会像?无界通配符一样失去类的特征,又不会像StackT<String>约束的很死,这就引出了上界通配符的概念。

上界通配符

可以使用上界通配符来缩小类型参数的类型范围。

它的语法形式为:<? extends Number>

public class Why {
public static void main(String[] args) { StackT<Integer> stackT = new StackT<>(3, Integer.class);
stackT.push(8);
StackT<String> stackT1 = new StackT<>(3, String.class);
stackT1.push("7");
StackT<Double> stackT2 = new StackT<>(3, Double.class); //通过
test(stackT);
test(stackT2);
//error
test(stackT1); } public static void test(StackT<? extends Number> stackT){ System.out.println(stackT.pop());
}
}

这样就实现了一类类型的限定,但是需求变更了,我现在希望的约束关系是我能接收的泛型栈的类型都是Number类的父类,或者父类的父类,那么有上界,自然就有下界

下界通配符

下界通配符将未知类型限制为该类型的特定类型或超类类型。

注意:上界通配符和下界通配符不能同时使用

它的语法形式为:<? super Number>

public class Why {
public static void main(String[] args) { StackT<Number> stackT1 = new StackT<>(3, Number.class);
stackT1.push(8);
StackT<Double> stackT2 = new StackT<>(3, Double.class);
StackT<Object> stackT3 = new StackT<>(3, Object.class);
//通过
test(stackT1);
test(stackT3);
//error
test(stackT2); } public static void test(StackT<? super Number> stackT){ System.out.println(stackT.pop());
}
}

这样子的话,就确保了我们的test方法只接收Number类型以上的方法。泛型的各种高级语法可能在写业务代码的时候可以规避,但是如果你要去写一些框架的时候,由于你不知道框架的使用者的使用场景,那么掌握泛型的高级语法就很有用了。

通配符和向上转型

前面,我们提到:泛型不能向上转型。但是,我们可以通过使用通配符来向上转型

public class GenericsWildcardDemo {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // Error List<? extends Integer> intList2 = new ArrayList<>();
List<? extends Number> numList2 = intList2; // OK
}
}

通配符边界问题,关于一些更加深入的解惑可以参考整理的转载的文章——Java泛型解惑之上下通配符

泛型约束

Pair<int, char> p = new Pair<>(8, 'a');  // 编译错误
public static <E> void append(List<E> list) {
E elem = new E(); // 编译错误
list.add(elem);
}
public class MobileDevice<T> {
private static T os; // error // ...
}
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // 编译错误
// ...
}
}
List<Integer> li = new ArrayList<>();
List<Number> ln = (List<Number>) li; // 编译错误
List<Integer>[] arrayOfLists = new List<Integer>[2];  // 编译错误
// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ } // 编译错误 // Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // 编译错误
public static <T extends Exception, J> void execute(List<J> jobs) {
try {
for (J job : jobs)
// ...
} catch (T e) { // compile-time error
// ...
}
}
public class Example {
public void print(Set<String> strSet) { }
public void print(Set<Integer> intSet) { } // 编译错误
}

实践总结

泛型命名

泛型一些约定俗成的命名:

  • E - Element
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types

使用泛型的建议

  • 消除类型检查告警
  • List 优先于数组
  • 优先考虑使用泛型来提高代码通用性
  • 优先考虑泛型方法来限定泛型的范围
  • 利用有限制通配符来提升 API 的灵活性
  • 优先考虑类型安全的异构容器

参考资料:

深入理解 Java 泛型

On Java 8

Java泛型解惑之 extends T>和 super T>上下界限

7月的直播课——Java 高级语法—泛型

从一知半解到揭晓Java高级语法—泛型的更多相关文章

  1. Java高级语法之反射

    Java高级语法之反射 什么是反射 java.lang包提供java语言程序设计的基础类,在lang包下存在一个子包:reflect,与反射相关的APIs均在此处: 官方对reflect包的介绍如下: ...

  2. JAVA高级语法

    高级语法 第三章:面向对象和高级语法 实例化: 不实例化,就是一个空指针 注意,声明和实例化是两个过程.声明的过程是不分配内存空间的,只有实例化才会真正分配空间 对变量的分类 实例变量只有实例化之后才 ...

  3. 黑马程序员:Java基础总结----泛型(高级)

    黑马程序员:Java基础总结 泛型(高级)   ASP.Net+Android+IO开发 . .Net培训 .期待与您交流! 泛型(高级) 泛型是提供给javac编译器使用的,可以限定集合中的输入类型 ...

  4. JAVA 高级特性枚举和泛型

    枚举: 语法:  public enum 枚举名 { 枚举值表(罗列所有值) }  例如:  public enum EnumTest{MON,TUE,WED.THU,FRI,SAT,SUN} 枚举操 ...

  5. Java基础语法<十二> 泛型程序设计

    1 意义 泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用. 常见应用 : ArrayList 2 K T V E ? object等的含义 类型变量使用大写形式 E – Element ( ...

  6. [Java面经]干货整理, Java面试题(覆盖Java基础,Java高级,JavaEE,数据库,设计模式等)

    如若转载请注明出处: http://www.cnblogs.com/wang-meng/p/5898837.html   谢谢.上一篇发了一个找工作的面经, 找工作不宜, 希望这一篇的内容能够帮助到大 ...

  7. Java高级之类结构的认识

    本文来自http://blog.csdn.net/liuxian13183/ ,引用必须注明出处! Java体系包括,各种版本的虚拟机,可执行文件,各种第三方类库,Java API类库,Java语言 ...

  8. 转载:[Java面经]干货整理, Java面试题(覆盖Java基础,Java高级,JavaEE,数据库,设计模式等)

    原文:http://www.cnblogs.com/wang-meng/p/5898837.html 一:继承.抽象类与接口区别.访问控制(private, public, protected,默认) ...

  9. Java系列之泛型

    自从 JDK 1.5 提供了泛型概念,泛型使得开发者可以定义较为安全的类型,不至于强制类型转化时出现类型转化异常,在没有反省之前,可以通过 Object 来完成不同类型数据之间的操作,但是强制类型转换 ...

随机推荐

  1. 一篇夯实一个知识点系列--python实现十大排序算法

    写在前面 排序是查找是算法中最重要的两个概念,我们大多数情况下都在进行查找和排序.科学家们穷尽努力,想使得排序和查找能够更加快速.本篇文章用Python实现十大排序算法. 干货儿 排序算法从不同维度可 ...

  2. 字节跳动:[编程题]万万没想到之聪明的编辑 Java

    时间限制:1秒 空间限制:32768K 我叫王大锤,是一家出版社的编辑.我负责校对投稿来的英文稿件,这份工作非常烦人,因为每天都要去修正无数的拼写错误.但是,优秀的人总能在平凡的工作中发现真理.我发现 ...

  3. C#LeetCode刷题之#344-反转字符串​​​​​​​(Reverse String)

    问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/3933 访问. 编写一个函数,其作用是将输入的字符串反转过来. 输 ...

  4. 封装react antd的form表单组件

    form表单在我们日常的开发过程中被使用到的概率还是很大的,比如包含了登录.注册.修改个人信息.新增修改业务数据等的公司内部管理系统.而在使用时这些表单的样式如高度.上下边距.边框.圆角.阴影.高亮等 ...

  5. 如何使用screen命令

    大家好,我是良许. 很多时候,我们都需要执行一些需要很长时间的任务.如果这时候,你的网络连接突然断开了,那么你之前所做的所有工作可能都会丢失,所做的工作可能都要重做一遍,这会浪费我们许多的时间,非常影 ...

  6. 如何理解算法时间复杂度的表示法O(n²)、O(n)、O(1)、O(nlogn)等?

    先从 来说,理论上哈希表就是O(1).因为哈希表是通过哈希函数来映射的,所以拿到一个关键字,用哈希函数转换一下,就可以直接从表中取出对应的值.和现存数据有多少毫无关系,故而每次执行该操作只需要恒定的时 ...

  7. 贫血模型和DDD模型

    贫血模型和DDD模型 1.贫血模型 1.1 概念 常见的mvc三层架构 简单.没有行为 2.领域驱动设计 2.1 概念(2004年提出的) Domain Driven Design 简称 DDD DD ...

  8. golang目录

    基础 golang安装及vscode编辑器配置 golang基础结构 golang基础数据类型 golang复合数据结构 goalng函数 golang方法 golang接口 golang并发 gol ...

  9. IE浏览器连接WebSocket报错:java.lang.IllegalArgumentException: Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986

    在项目开发中整合了WebSocket,本来没什么问题了,但是偶尔发现用IE浏览器打开web端不能推送消息,因为PC端与服务器建立连接失败了.网上查了很多资料, 又看了看源码,都不对症:又怀疑是Spri ...

  10. 兄弟,你爬虫基础这么好,需要研究js逆向了,一起吧(有完整JS代码)

    这几天的确有空了,看更新多快,专门研究了一下几个网站登录中密码加密方法,比起滑块验证码来说都相对简单,适合新手js逆向入门,大家可以自己试一下,试不出来了再参考我的js代码.篇幅有限,完整的js代码在 ...