this
关键字
- 成员变量与参数同名时,使用 this 指定为成员变量
- 除构造器外,编译器禁止从其他任何地方使用 this 调用构造器
- 使用
this
调用构造器:- 可以使用 this 调用一个构造器(不能调用两个)
- 调用构造器必须位于起始处
初始化顺序
- 数组初始化:
- 最后的一个逗号可以保留
- 基本数据类型值会自动初始化为空值(数字:0,布尔:false)
- 代码块:
- 静态代码块:与成员静态变量完全相同 ==> 仅会在必要执行一次(类首次加载或静态方法被调用)
- 非静态代码块:用来初始化每一个对象的非静态变量(与非静态成员变量同,确保一定会在每一次构造方法前执行运行)
- 成员变量:
- 静态成员变量仅在必要时初始化(对象创建或第一次访问静态数据时才会初始化;且只会初始化一次)
- 非静态成员变量每次创建对象时都会进行初始化,且都在构造方法前顺序执行
// 编译正常
Integer[] a = {1, 2, 3,};
Integer[] b = new Integer[]{1, 2, 3,};
Integer[] c = new Integer[]{1,};
// 静态代码块
static {
cup1 = new Cup("A");
cup2 = new Cup("B");
}
// 实例化初始化
{
cup1 = new Cup("A");
cup2 = new Cup("B");
}
// 编译顺序
public class Window {
static {
// 1、静态代码块最先编译(类加载即执行)
}
// 静态成员变量仅在必要时初始化(对象创建或第一次访问静态数据时才会初始化;且只会初始化一次)
static Window sta = new Window();
// 非静态成员变量
Window w = new Window();
{
// 2、非静态代码块与非静态成员变量顺序执行,都在构造函数之前执行
}
Window(){
// 3、构造函数在静态代码块及非静态代码块后执行
}
}
// 方法重载
char ch = 1;
访问权限
·· | 同一个类 | 同一个包 | 不同包的子类 | 不同包的非子类 |
---|---|---|---|---|
public | 是 | 是 | 是 | 是 |
protected | 是 | 是 | 是 | 否 |
default | 是 | 是 | 否 | 否 |
private | 是 | 否 | 否 | 否 |
复用类
@Override
注解是JavaSE5
加入的- 除了内存以外,不能依赖垃圾回收器做任何事情;如果需要清理,最好编写自己的清理方法,但不要使用
finalize()
方法 - 子类初始化之前,一定会保证基类先行初始化(必须隐式|显式进行调用);Java 会自动在子类(导出类)的构造器中插入对父类(基类)构造器的调用
main
方法访问:- 即使是一个程序中含有多个类,只有命令行所调用的那个类的
main()
方法会被调用 - 即使
Clazz
类不是一个public
类,如果命令行是java Clazz
,那么Clazz.main()
仍然会被调用 - 即使一个类只具有包访问权限,其
public main()
仍然是可以访问的
- 即使是一个程序中含有多个类,只有命令行所调用的那个类的
- 继承:
is-a
,组合:has-a
【大多数情况下,子类会有自己的特有域,新的可以被称为:is-like-a
像一个】 - 继承与组合的选择:优先使用组合,尤其是不能十分确定应该使用哪一种方式时
- 向上转型:将导出类(子类)引用转换为基类(父类)引用
- 继承其实不太常用,如何确认?确认是否需要从导出类向基类进行向上转型
final
关键字:数据、方法、类- 一个既是
static
又是final
的域只占据一段不能被改变的存储空间 ==> 常量 - 基本类型:
final
使数字恒定不变;对象引用:final
使引用不变(一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象;然而,对象其自身却是可以改变的,Java并未提供使任何对象恒定不变的路径) - 空白
final
:声明为final
但又未给定初始值(编译器能确保空白final
在使用前必须被初始化) ==>Spring
依赖中使用构造函数进行依赖组装 final
参数:无法在方法中更改参数引用所指向的对象(主要用来向匿名内部类传递数据)final
方法:只有在想要明确禁止覆盖时,才将方法设置为final
的;所有private
方法都隐式的指定为final
;final
方法对程序的执行效率没有明确的提升final
类:禁止被继承
- 一个既是
- 数组也是引用
java
构造方法
有两个构造方法,分别是实例化构造方法(instance初始化方法)和Class对象构造方法;
实例化构造方法:负责完成实例字段的初始化
Class对象构造方法:可以理解为Class(静态)构造方法,负责静态字段的初始化(Java编译器自动生成)
每个对象都会对应有且仅有一个Class对象,即Class对象是单例的。所以在第一次创建对象时,需要先创建Class对象(类加载过程完成,调用的就是Class对象构造方法);后续继续创建该Class的实例对象时,就不需要再创建Class对象了。
初次使用(类加载)之处也是static初始化发生之处。所有的static对象和static代码段都会在类加载时依定义顺序执行;static域只会被初始化一次(类仅会被加载一次)
多态
- 动态绑定:在运行时根据对象的实际类型进行绑定(Java中除了
static
和private
方法【private
方法同属于final
方法】之外,其他所有方法都是动态|后期|运行时绑定 ==>自动进行
) - 只有非
private
方法才可以被覆盖(private
方法默认是final
的);导出类中,对于基类中的private
方法最好采用不同的名字 - 编写构造器时有一条有效的准则:用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法,(在构造器内唯一能够安全调用的那些方法是基类中的
final
方法【含private
方法】) Java
语言中,所有类型转换都会得到检查;即使是一次普通的加括弧类型转换,进入运行期时仍然会对其进行检查;如果转换失败,会返回ClassCaseException
异常。在运行期对类型进行检查的行为被称为运行时类型识别(RTTI)- 协变返回类型:子类(导出类)中的被覆盖方法可以返回父类(基类)方法的返回类型的某种导出类型
接口
- 抽象类:如果一个类包含一个或多个抽象方法,该类必须被限定为抽象的。
interface
关键字产生了一个完全抽象的类,不提供任何具体实现- 接口被用来建立类与类之间的
协议
- 接口也可以包含域,这些域隐式的都是
static
且final
的 - 使用接口的原因:
- 为了能够向上转型为多个基类型(以及由此而带来的灵活性)
- 防止客户端程序员创建该类的对象,并确保这仅仅是建立一个接口
- 如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类(如果知道某事物应该成为一个基类,第一选择应该定义为接口)
- 接口可以嵌套在类或其他接口中
- 接口嵌套在类中:可以将接口定义为
private
的 - 接口嵌套在接口中:自动声明为
public
- 接口嵌套在类中:可以将接口定义为
- 当实现某个接口时,并不需要实现嵌套在内部的任何接口。且
private
接口不能在定义它的类之外被实现 - 接口可以多重继承
- 恰当的原则应该是优先选择类而不是接口
内部类
-
在
JDK11+
中,从所在的外部类
静态方法创建某个非静态内部类的对象,可以不用按照OuterClassName.InnerClassName
书写;在其他独立的类中必须遵从此规则 -
生成一个非静态内部类对象
- 能访问其外围对象的所有成员
- 拥有其外围类的所有元素的访问权
public class Outer{ public class Inner{ public Outer outer(){ // 通过 OuterClassName.this 可以在内部类中返回当前的外部类对象 return Outer.this; } } public static void main(String[] args) { Outer ou = new Outer(); // 非静态内部类依赖于外部类对象使用 .new 生成内部类对象 // 在当前外部类或其他独立类中均生效 Inner inner = ou.new Inner(); } }
-
.this
:在非静态内部类中正确返回当前所属外部类对象,可利用OuterClassName.this
实现 -
.new
:通过外部类对象创建非静态内部类对象,可利用OuterClassInstance.new InnerClassName()
实现// 为匿名内部类实现构造器行为 abstract class Base{ public Base(int i){ System.out.println("Base constructor, i= " + i); } public abstract void f(); } public class Outer{ public static Base getBase(int i){ return new Base(i){ { // Java的类加载顺序保证了优先调用父类构造,此处子类构造被替换为实例初始化 // 效果上等同于当前匿名内部类拥有了自己的构造 System.out.println("Inside instance initializer"); } @Override public void f() { System.out.println("In anonymous f()"); } }; } }
-
分类(按照书写位置分类)
- 非静态内部类(局部|匿名)对象隐式的保存了一个指向创建它的外围类对象引用,在此作用域内,内部类有权操作所有的成员,包括private成员
- 静态内部类(嵌套类)
- 要创建嵌套类对象,并不需要其外围类的对象
- 不能从嵌套类的对象中访问非静态的外围类对象
- 嵌套类可以包含static的数据和字段
- 嵌套类可以作为接口的一部分
- 如果想要创建某些公共代码,使得它们可以被该接口的所有实现公用,推荐使用接口内的嵌套类实现
- 一个内部类(非静态)被嵌套多少层并不重要 – 它能透明的访问所有它所嵌入的外围类的所有成员
- 局部内部类:定义在方法的作用域内(此内部类除作用域不同,其他与一般类完全相同)。局部内部类不能有访问说明符
- 匿名内部类:与正规的继承类相比有些受限,虽既可以扩展类,也可以实现接口,但二者不能兼备;如果为实现接口,也只能实现一个接口
- 成员内部类:(非)静态内部类
-
为什么需要内部类?每个内部类都能独立的继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响
-
内部类继承
// 内部类继承 class WithInner{ class Inner{ // 内部类 } } // 直接继承内部类 public class InheritClass extends WithInner.Inner{ // 无法使用默认的构造器实现,必须提供外围类的引用 InheritClass(WithInner wi) { // 提供外围类对象的引用 wi.super(); } } // 非静态内部类任意继承不影响
-
内部类可以被覆盖吗?
// 1、当继承了某个外围类的时候,内部类并没有什么变化 // 2、不同的内部类是完全独立的,各自在自己的命令空间内 // 3、可以明确的继承某个内部类 class Egg{ protected class Yolk{ // 这是一个内部类 } } // 4、当前外围类必须同时继承被继承内部类的外围类 class BigEgg extends Egg{ public class Yolk extends Egg.Yolk{ // 继承自另一个内部类 } }
-
局部内部类与匿名内部类
- 局部内部类可以拥有具名构造器或重载构造器;匿名内部类只能用于实例初始化
- 局部内部类可以创建多个对象
-
如果内部类是匿名的,编译器会简单的产生一个数字作为类名标识符,结果:
OuterClassName$Num.class
持有对象
- 使用
@SuppressWarnings
注解及其参数表示只抑制有关“不受检查的异常”的警告信息 - 如果一个类没有显式的声明继承自哪个类,那么它自动的继承自
Object
Java
容器类库的用途是“保存对象”,划分为两个不同的概念Collection
:元素序列,List
按照插入顺序保存元素、Set
不能有重复元素、Queue
按照排队规则确定对象产生顺序Map
:一组成对的“键值对”对象,允许使用键来查找值;也可以叫做映射表、字典
Set
中只有元素不存在的情况下才会添加:TreeSet
默认按照字典序
升序保存对象,LinkedHashSet
保留了添加顺序(TreeMap
、LinkedHashMap
同)- 推荐使用的工具类:
java.util.Arrays
、java.util.Collections
Arrays.asList
说明:- 充当了一个基于数组的API和Collection API的桥梁,该方法仅仅是为了让数组能够用上Collection API,但其本质(底层)还是数组
- 返回的
ArrayList
是一个内部类,不是标准的java.util.ArrayList
- 泛型指定:
Arrays.<T>asList(T... t)
Collections.addAll
:执行效率很高,The behavior of this convenience method is identical to that of c.addAll(Arrays.asList(elements)), but this method is likely to run significantly faster under most implementationsList
ArrayList
:长于元素随机访问,但是在List
中间插入和移除元素较慢 – 数组LinkedList
:在List
中插入和移除元素代价较低,提供了优化的顺序访问;随机访问速度较慢 – 链表;可以作为栈
或队列
使用
- 迭代器能够将遍历序列的操作与序列底层的结果分离,迭代器统一了对容器的访问方式
- 迭代器
Iterator
只能向前移动;ListIterator
只能用于各种List
的访问,可以实现双向移动 Stack
,"栈"通常指后进先出 LIFO
的容器,有时也称为叠加栈
Queue
,队列
是一个典型的先进先出 FIFO
容器;先进先出
是最典型的队列规则;PriorityQueue
优先级队列,通过默认的自然顺序或自定义Comparator
实现插入元素时同步维护元素优先级- 创建任何实现了
Iterable
的类,都可以用于foreach
语法中(数组
除外) - 各种
Queue
及栈
的行为,由LinkedList
提供支持
异常
public static void f() throws Exception {
System.out.println("Originating the exception in f()");
throw new Exception("Throw from f()");
}
public static void g() throws Exception {
try {
f();
} catch (Exception e) {
System.out.println("Inside g().e, e.printStackTrace()");
e.printStackTrace(System.out);
// 重新抛出异常
throw e;
}
}
public static void h() throws Exception {
try {
f();
} catch (Exception e) {
System.out.println("Inside.h(), e.printStackTrace()");
e.printStackTrace(System.out);
// 把当前调用栈信息填入原来的异常对象(有关原来的异常发生点信息会丢失)
// todo 调用 fillInStackTrace 的这一行就成了异常的新发生地点了
throw (Exception) e.fillInStackTrace();
}
}
// 异常丢失
public static void i(){
try{
System.out.println("异常丢失");
try{
// 这个方法抛出异常1
a.occurException1();
}finally{
// 这个方法抛出异常2
a.occurException2();
}
}catch(Exception e){
// 这里最后只能捕捉到 finally 中产生的异常2,异常1被丢掉了
System.out.println(e);
}
}
// 构造器中发生异常时 finally 的使用规则
// 在创建需要清理的对象之后,立即进入一个 try-finally 语句块
public static void main(String[] args) {
try {
InputFile in = new InputFile("Cleanup.java");
try {
String s;
int i = 1;
while ((s = in.getLine()) != null) {
System.out.println("对文件的一些操作");
}
} catch (Exception e) {
System.out.println("Caught exception in main()");
e.printStackTrace(System.err);
} finally {
// 构造阶段可能抛出异常,且要求清理资源,强烈建议使用嵌套try-catch-finally子句
in.dispose();
}
} catch (Exception e) {
System.out.println("InputFile construction failed!");
}
}
-
能够抛出任意类型的
Throwable
对象,分为两种类型:Error
表示编译时和系统错误,一般不用关心;Exception
是可以被抛出的基本类型 -
通常,异常对象中仅有的信息就是异常类型,除此之外不包含任何有意义的内容(最重要的部分就是异常类型)
-
异常处理理论上有两种基本类型:
- 终止模型:错误无法挽回,不能继续正常执行程序
- 恢复模型:异常处理程序修正错误,然后重新尝试调用出问题的方法,并认为第二次调用能成功(不实用)
-
分类:虚拟机能处理的 – 不受检查异常;虚拟机不能处理的 – 被检查异常???
- 被检查的异常:编译时被强制检查的异常,直接
extends Exception
实现;必须要在方法上主动throws
指明 - 不受检查异常:即运行时异常,
RuntimeExcpetion
及其子类,会自动被Java
虚拟机抛出,不必在异常说明将它们列出来;这种异常属于编程错误,通常不用在代码中捕获,但是可以抛
- 被检查的异常:编译时被强制检查的异常,直接
-
可以声明方法将抛出的异常,实际上却不抛出,为异常先占个位置;定义抽象基类和接口时可以进行异常预留
-
栈轨迹:
printStackTrace()
方法所提供的信息可以通过getStackTrace()
方法访问,将返回一个由栈轨迹中的元素所构成的数组,其中每一个元素都表示栈中的一帧。元素0是栈顶元素,并且是调用序列中的最后一个方法调用;数组中的最后一个元素和栈底是调用序列中的第一个方法调用 -
重新抛出异常:
- 调用
fillInStackTrace()
的代码行会成为新的异常发生处 - 若在捕获异常后抛出另一种异常,得到的效果类似于使用
fillInStackTrace()
,且有关原来的异常发生点的信息会丢失 - 如果希望新的异常能保留原始的异常信息(异常链),使用含*cause(因由)*的构造函数即可实现
在Throwable的子类中,只有三种基本的异常类提供了带cause的构造器:Error、Exception、RuntimeException。如果要把其他类型的异常链接起来,应该使用initCause方法而不是构造器
- 调用
-
Exception
对象都是使用new
在堆上创建的对象,垃圾回收器会自动执行清理 -
无论
try
块中的异常是否抛出,finally
中的语句总能被执行(可用于实现重试机制、释放资源) -
当涉及
break
和continue
语句的时候,finally
子句也会得到执行(如果– 不建议在finally
子句中存在return
语句,那么此返回会覆盖所有其他的return
finally
中使用return
) -
异常丢失
-
当覆盖方法时,只能抛出在基类方法的异常说明里列出的异常(也可以不抛出异常或抛出更小的异常)
-
一个方法同时存在于抽象基类和接口中(两种方法定义抛出不同类型的异常),子类继承并实现时,接口方法抛出的异常不能改变抽象基类中的异常(两者的异常必须相同或子类不抛出任何异常)
-
对于在构造阶段可能会抛出异常,并且要求清理的类,最安全的使用方式是使用嵌套的
try-catch-finally
子句 -
异常匹配:抛出异常时,异常处理系统会按照代码编写顺序找出“最近”的处理程序(匹配成功即不再继续查找)【派生类的对象也可以匹配其基类的异常处理程序】
-
被检查异常处理方式;
- 将被检查异常转换为不检查的异常(封装
RuntimeException(e)
) - 创建自己的
extends RuntimeException
异常
- 将被检查异常转换为不检查的异常(封装
字符串
public class TestClazz{
private Formatter formatter;
private String name;
public void testFormat(int x, int y){
// 内容输出格式化
formatter.format("The turtle %s is moving to (%d, %d)", x, y);
// 更复杂的格式化
formatter.format("%-15s %5s %10s\n", "Item", "Qty", "Price");
}
public String toString(){
// 无意识的递归:编译器尝试将this转换为String进行拼接(转换方式为 this.toString())
//return " TestClazz address: " + this + "\n";
// 输出当前对象的内存地址,正确的方式是调用 Object.toString()
return " TestClazz address: " + super.toString() + "\n";
}
public static void main(String[] args){
// Formatter构造时接受一个输出内容目的地的参数;存在多种重载
Formatter f = new Formatter(System.out);
}
}
-
不可变的
String
:String
对象是不可变的,String
类中每一个看起来会修改String
值的方法,实际上都会创建一个全新的String
对象,以包含修改后的字符串内容,只读特性 -
每当把
String
对象作为方法的参数时,都会复制一份引用,而盖引用所指的对象其实一直都在单一的物理位置上,从未改变 -
用于
String
的+
和+=
是Java
中仅有的两个重载过的操作符,而Java
并不允许程序员重载任何操作符+
与+=
会被重载为使用StringBuilder
实现- 如果可以预知最终的字符串有多长,预先指定
StringBuiler
的大小可以避免多次重新分配缓冲
-
StringBuffer
:线程安全,开销更大 -
如果要在
toString()
方法中使用循环,最好自己创建一个StringBuilder
对象 -
如果希望
toString()
方法打印对象内存地址,可以考虑使用this
关键字 -
无意识的递归
-
格式化输出:
System.out.format()
System.out.printf()
-
Formatter
:在Java
中所有新的格式化功能都由java.util.Formatter
类处理,接受一个参数表明内容的输出目的地,常用的包括PrintStream
、OutputStream
、File
-
格式化符说明:
%[argument_index$][flags][width][.precision]conversion
- 默认情况下数据右对齐,可以通过
-
改变对齐方向 width
应用于各种数据类型的数据转换,且行为方式都相同precision
不是所有类型数据都支持;对于不同的数据类型,其意义也不相同;对整数应用precision
会触发异常
- 默认情况下数据右对齐,可以通过
-
正则表达式
-
匹配一个反斜杠需要四个反斜杠
\\\\
,第一个:转义符,第二个:斜杠本身,第三个:转义符,第四个:斜杠本身\
在Java
和正则表达式中都是转义字符,所以表示斜杠时都需要书写为:\\
理解:正则中的
\
,使用字符串表示即\\
,而每一个斜杠都需要一个转义符,结果即\\\\
表示一个斜杠 -
组:用括号划分的表达式,组号
0
表示整个表达式,组号1
表示被第一队括号括起的组,以此类推A(B(C))D
:有三个组,组0是ABCD;组1是BC;组2是Cjava.util.Matcher
提供了方法获取组相关信息,groupCount(),不含组0
、group()
。。。 -
几个特殊符号
字符 作用 \n 换行 \r 回车 \t 制表(相当于tab) \f 换页 -
Scanner
定界符:默认情况下,Scanner
使用空白字符对输入进行分词,方法useDelimiter()
可以自定义定界符
-
类型信息
// 初始化的时机
public class Initialization{
// static final 的编译期常量读取时不会触发初始化
static final int staticFinal = 47;
// 非编译器常量,读取时会触发初始化
static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);
// static非final域,读取时需要执行
static int staticNonFinal = 147;
// JDK11+会产生告警
Class<Integer> intClass1 int.class;
Class<Integer> intClass2= int.class;
// Integer extends Number,但Number Class域Integer Class之间没关系
// Class<Number> numberClass = int.class;
// 使用通配符?,好处是实现选择非具体的类引用(不会产生任何的告警)
Class<?> numberClass = int.class;
// 创建类型子类型,? extends T 创建一个范围(规定上限)
Class<? extends Number> bounded = int.class;
// ? super T 也是创建一个范围(规定下限)
// FancyToy extends Toy
Class<FancyToy> fancyToyClass = FancyToy.class;
// 可以直接获取到具体类型
FancyToy fancyToy = fancyToyClass.getDeclaredConstructor().newInstance();
Class<? super FancyToy> superClass = fancyToyClass.getSuperclass();
// todo 只允许将超类声明为“某个类,是FancyToy的超类(非接口)”
// Class<Toy> superclass2 = fancyToyClass.getSuperclass();
// 这里就只能是 Object 了
Object object = superClass.getDeclaredConstructor().newInstance();
}
-
运行时类型识别(RTTI,Run-Time Type Identification):在运行时,识别一个对象的类型。(在
Java
中,所有的类型转换都是在运行时进行正确性检查的)Java
中每个对象都有相应的Class
对象,因此,可通过Class
对象知道某个对象‘真正’所属的类;无论我们对引用进行怎样的类型转换,*对象本身所对应的Class
*对象都是同一个。当我们通过某个引用调用方法时,Java
总能找到正确的Class
类中所定义的方法,并执行该Class
中的代码。由于Class
对象的存在,Java
不会因为类型的向上转型而迷失,这也是多态的原理 -
Java
使用Class
对象来执行其RTTI
,即使正在执行的是类似转型的操作 -
对于基本数据类型的包装器类,有一个标准字段
TYPE
,是一个引用,指向对应的基本数据类型的Class
对象基本数据类型 包装器类 boolean.class Boolean.TYPE char.class Character.TYPE … … 以上两种表示方式是等效的。(建议使用
.class
的形式,保持与普通类的一致性) -
加载类的准备工作包含三个步骤:
- 加载:由
类加载器
执行,查找字节码
并从字节码创建一个Class
对象 - 链接:
验证类中的字节码,为静态域分配存储空间
,必须的话,将解析这个类创建的对其他类的所有引用 - 初始化:若该类具有超类,则对其初始化,
执行静态初始化器和静态初始化块
- 加载:由
-
初始化触发
- 使用
.class
来创建Class
对象的引用时,不会自动的初始化该Class
对象 - 使用
Class.forName
会立即就触发初始化 - 读取一个
static final
的值(必须是编译期常量
),不会对所在类进行初始化 - 如果一个
static
域为非final
,读取之前需要进行链接(分配存储空间)和初始化(初始化该存储空间)
- 使用
-
将
泛型
语法应用于Class
对象时(参考上面的代码说明)- 如果是
? extends T
,newInstance()
将返回该对象的真实类型 - 如果是
? super T
,newInstance()
仍旧只能返回Object
- 如果是
-
新的类型转换语法:
java.lang.Class#cast
-
RTTI
表现形式- 传统的强制类型转换
- 代表对象的类型的
Class
对象。通过查询Class
对象获取运行时所需的信息 - 关键字
instanceof
-
instanceof
与isInstance
结果完全等效 -
在动态代理上所作的所有调用都会被重定向到单一的
调用处理器(InvocationHandler)
上,它的工作是揭示调用的类型并确定响应的策略 -
将
interface
实现为私有内部类、匿名类都无法阻止通过反射
在运行过程中的修改操作
泛型
// 泛型类定义
public class GenericClazz<T>{
public static <K, V> Map<K, V> map() {
return new HashMap<>();
}
// 泛型数组
// 使用类型标记 Class<T> 执行数组的创建以便从擦除中进行类型恢复
// 泛型数组的运行时类型只能是 Object[]
private T[] array;
public GenericClazz(Class<T> type, int sz) {
array = (T[])Array.newInstance(type, sz);
}
public static void main(String[] args) {
// 自动类型推断
Map<String, List<? extends Pet>> map = New.map();
}
}
// 泛型方法
public <T> void f(T t){
// 泛型方法定义:需要将泛型参数列表置于返回值之前
}
// 泛型多边界定义
class MultiBounded<T extends ConcreteOrAbstractClazzOrInterface & Interface1 & Interface2>{
// 泛型边界支持多边界
// 1、必须具体类在前,接口在后
// 2、具体类可以是普通类或抽象类或接口
// 3、具体类只能存在一个,接口可以多个
}
class Fruit{}
class Apple extends Fruit{}
class Plate<T>{
private T item;
public Plate(T item) {
this.item = item;
}
public T get() {
return this.item;
}
public void set(T item) {
this.item = item;
}
public static void main(String[] args) {
// p是协变的
// JVM会使用一个占位符 CAP#1 来表示p接受一个Fruit或子类,运行时通过 CAP#1 把类型擦除了
Plate<? extends Fruit> p = new Plate<>(new Apple());
p.set(null);
// 无论如何都不能赋值一个 CAP#1 类型 ==> Java类不存在一个公共的子类😪
// p.set(new Fruit());
// p.set(new Apple());
// CAP#1 表示的是 Fruit及其子类
Fruit fruit = p.get();
Object object = p.get();
// Apple apple = p.get();
}
}
// 1、Payable<Employee>与Payable<Hourly>泛型移除后简化为相同的类Payable,下面的代码编译报错
// 2、如果移除Payable<T>的泛型,编译正常(有趣。。。)
interface Payable<T>{}
class Employee implements Payable<Employee>{}
//class Hourly extends Employee implements Payable<Hourly>{}
// CRG
class BasicHolder<T> {}
class Subtype extends BasicHolder<Subtype> {
// 基类用导出类替代其参数(泛型基类变成了一种其所有导出类的公共功能的模板)
}
// 自限定
class SelfBounded<T extends SelfBounded<T>>{
// 强制泛型当作其自己的边界参数来使用;只能强制作用于继承关系
// 自限定希望的用法:class A extends SelfBounded<A>{}
}
// 参数协变
interface SelfBoundSetter<T extends SelfBoundSetter<T>>{
void set(T arg);
}
interface Setter extends SelfBoundSetter<Setter>{}
void test(Setter s1,Setter s2,SelfBoundSetter sbs){
s1.set(s2);
// 使用了自限定类型,编译器不能识别将基类型当作参数传递目标方法的尝试。
// 不使用自限定类型,普通的继承机制会介入,能够实现重载
// s1.set(sbs);
}
-
元组(tuple)
:与List
同,都可用于数据存储,包含多个数据。不同的是:列表只能存储相同的数据类型,元组可以存储不同的数据类型,如同时存储int
,string
,list
等,并且可以根据需求无限扩展 -
泛型可以使用在类或方法上
-
应该尽量使用泛型方法
-
static
的方法无法访问泛型类的类型参数;如果static
方法需要使用泛型能力,就必须使其称为泛型方法 -
类型参数推断:使用泛型类时,必须在创建对象时指定类型参数的值;使用泛型方法时,编译器会自动推导出具体类型
-
类型推断只对赋值操作有效,其他时候并不起作用。如果将一个泛型方法调用结果作为参数传递给另一个方法,此时编译器只会认为接受的是一个
Object
类型的变量 -
在泛型代码内部,无法获得任何有骨干泛型参数类型的信息。Java泛型是使用
擦除
来实现的,任何具体的类型信息在运行时都被擦除了,都会称为原生
类型;泛型类型参数将擦除到它的第一个边界
-
泛型并不是
Java
语言的一种特性,它是Java
泛型实现中的一种折中;泛型类型只有在静态类型检查期间才出现 -
泛型兼容性:泛型化的代码必须能够与非泛型化的代码共存。使用
擦除
是目前唯一可行的解决方案(从非泛化代码到泛化代码的转变过程;在不破坏现有类库的情况下将泛型融入Java语言) -
在泛型中创建数组,推荐使用
Array.newInstance()
-
即使擦除在方法或类内部移除了有关实际类型的信息,
编译器
仍然可以保证在方法或类中使用的类型的内部一致性 -
不能创建泛型数组。一般的解决方案为在任何想要使用数组的地方都使用
ArrayList
-
成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其进行转型(转型后生成的数组并不能做任何操作;不管怎么转换,实际的运行时类型始终是
Object[]
。数组在运行时类型只能是Object[]
) -
泛型数组在运行时只能是
Ojbect[]
;直接获取其数组同时会触发编译器警告及运行时ClassCastException
,原因是运行时无法获取其真实类型。使用类型标记Class<T>
创建数组不能改变运行时类型,但可以从类型擦除中恢复
-
泛型边界:重用了
extends
关键字;可以定义多边界;通配符(?)被限定为单一边界 -
通配符**?与类型参数T**区别:
-
<? extends T>
不能往里存,只能能往外取(被称作协变)-
往里存:不能调用
<? extends T>
泛型类的以T
为形参的方法 -
往外取:可以调用
<? extends T>
泛型类的以T
为返回值的方法不能往里存的根本原因是因为,Java没有一个共同的子类,却有一个共同的父类
Object
。所以永远可以向上转型取数据,却不能向下转型存数据
-
-
<? super T>
不影响往里存,但是往外取却只能放在Object
(被称为逆变)-
容器中存放的是
T
的任意基类,但是不确定是哪一个基类 -
往里存:往容器中放
T
的子类一定成立(这些类一定是<? super T>
的子类) -
往外取:编译器不知道具体存放的类,所以取出来的是所有类的共同父类
Object
-
-
PECS(Producer Extends Consumer Super)
- Producer Extends:当前类主要作为生产者向外提供数据,使用
extends
- Consumer Super:当前类主要作为消费者,需要消费(吃进)数据,使用
super
- Producer Extends:当前类主要作为生产者向外提供数据,使用
-
-
List<?>
与List
List
实际上表示“持有任何Object
类型的原生List
”List<?>
表示“具有某种特定类型
的非原生List
,只是不知道那种类型是什么”
-
任何基本类型都不能作为类型参数
<T>
;自动包装机制不能应用于数组 -
古怪的循环泛型(CRG):基类用导出类替代其参数(泛型基类变成了一种其所有导出类的公共功能的模板)
-
自限定:强制泛型当作其自己的边界参数来使用;只能强制作用于继承关系
-
参数协变:使用了自限定类型,编译器不能识别将基类型当作参数传递目标方法的尝试(此方法接收基类作为参数)
-
类型参数可能会在一个方法的
throws
子句中用到 -
混型:混合多个类的能力,以产生一个可以表示混型中所有类型的类。价值之一是它们可以将特性和行为一致的应用于多个类之上。基于继承实现。