《java核心技术卷1》部分章节读书笔记

java 专栏收录该内容
4 篇文章 0 订阅

3 java基本的程序设计结构

3.1 命名规范

可参考 《阿里云java开发规范》

  • 以字母开头
  • 驼峰命名法
  • 类名首字母大写
  • 私有方法以 m 字母开头
  • Exception,Test类或者方法,以该单词结尾
  • 不出现魔法变量,常量全大写,尽量表达清楚语义,不要嫌命名长
  • 不出现中英文夹杂

3.2 数据类型

共有 8 种基本类型:

  • 4种整型:int(4字节),short(2字节),long(8字节),byte(1字节)
  • 2种浮点型:float(4字节,有效位6位),double(8字节,有效位15位)
  • 1种字符类型:char
  • 1种布尔型:boolean

strictfp关键字:处理a * b / c,jvm 默认先截断 a * b,再除以 c,而加了这个关键字后,丢给底层 intel去处理中间的截断,这样就更加精确。

3.3 运算符

  • 三元运算符(a>b?a:b) 比 if-else 高效:涉及cpu流水线作业和分支预测,例如遇到if-else分支的时候,会先预测一个分支,成功了那么效率得到提高,但是预测失败就会付出代价;而三元运算符直接计算两个分支,最后比较结果,虽然指令增加了,但是cpu擅长处理指令。
  • 位运算:&,|,^,>>, <<,~。
  1. x&(x-1):用于右边取消 x 的最右边一位 1。应用:判断是否是2^n;判断整数A转化为整数B变了多少位
  2. x & 1 :可以判断奇偶数
  3. a^ b^b=a:异或可以将偶数重复的数消除。应用:一堆偶数重复的数中找一个或者两个独立数
  4. 异或还可以应用于不借助temp交换两个数,思想就是异或可以拿到公共的 1: a^ =b;b^ =a;a^=b
  5. >>1:等同于除以2;<<1:等同于乘2
  6. 正负数交换:~x+1,即大计基中的取反加1;这里也涉及到绝对值求法

3.4 枚举类型:

enum 关键字:

  1. 使用方法:它类似class关键字,但是内部只有常量,可以定义在class类内部或者外部
  2. 原理:继承自java.lang.Enum,会编译成一个 .class文件,也就是说它会是一个实实在在的类。在底层,编译器会自动生成很多代码,比如将每个常量赋值为static final类等,实际上让程序员偷懒了。

参考博客


	public class hello3 {
	    enum Size {SMALL, MEDIUM, LARGE};
	    public static void main(String args[]) {
		Size s = Size.SMALL;
		System.out.println(s.toString()); // output: SMALL
		System.out.println(s.ordinal()); // 0
		
	    }
	}

3.5 字符串

String 关键字

  1. java 中字符串不是字符数组,是一个不可变的整体
  2. String 中直接赋值和new赋值不同,前者放在常量池中,如果常量池中存在了,那么不会创建;后者放在堆中,会一直新建对象。
  3. String 已经重写了equals(),hashcode(),可以直接作为hash key,但是如果自定义类作为hash key也应该重写这两个方法。下面比较一下,equals,hashcode(),==:
  • equals(): 默认equals和==都是比较对象的地址,object类定义的,所以每个对象都有,一般用于比较引用数据类型,此时应该重写equals()方法,改为比较其内部成员的值
  • hashcode(): 默认hashcode()是object提供的,但是String重写了,是根据内容来计算hashcode。String重写hashcode()的源码里会包含 31 这个魔法数字,原因如下:①31会被jvm优化为按位操作,31 * i = (i<<5)-1;②31是一个5位的不大不小的质数,选择质数作为哈希乘子,是因为,反过来取模的时候,可以让余数均匀分布。(注意:哈希操作是正向乘,逆向mod)
  • ==:如果是基本类型,那么是值比较;如果是引用类型,那么比较地址。
  • 比较equals()和hashcode():hashcode()相等,equals不一定相等,equals相等,hashcode一定相等。

String Buffer 和 String Builder

  1. StringBuffer 和 StringBuilder 长度可变

  2. 多线程环境下,StringBuffer 线程安全, StringBuilder 线程不安全,StringBuffer通过对 append 方法加锁来达到线程安全的,builder没有加锁。

  3. 单线程使用时,StringBuilder 速度快

3.6 大数值 BigInteger和BigDecimal

3.7 数组

Arrays 好用的 API

  • 打印数组:Arrays.toString(array);Arrays.deepToString(arrays),打印高维数组
  • 匿名数组:a = new int[] {1, 2, 3, 4}; // 匿名数组写法,好处是不创建新变量重新初始化数组
  • 拷贝数组:普通拷贝是引用拷贝使用统一数据源,调用 Arrays.copyOf() 是拷贝一个新数组,使用不同数据源
  • 数组排序:Arrays.sort()采用了优化的快排
  • 二分查找:Arrays.binarySearch()
  • 不规则数组

4 对象与类

4.1 识别类

在一个系统中,类应该是一个名词,方法应该是一个动词

4.2 类之间的关系

其实有时候更多是结合业务背景的语义来判断二者是什么关系,直接通过代码不好判断

  • 依赖关系(user-a),类A依赖类B,表现为类B作为类A的方法中的形式参数,或者类A调用类B的静态方法,或者类B作为类A的方法中的局部变量。

	class Car {
    	public static void run() {
		System.out.println("汽车在跑");
    	}
	}

	class Driver {
    	public void driver(Car car) {
		// 使用形参方式发生依赖关系
		car.run();
    	}

    	public void driver() {
		// 使用局部变量发生依赖关系
		Car car = new Car();
		car.run();
    	}

    	public void driver1() {
		// 使用静态方法发生依赖关系
		Car.run();
    	}
	}


  • 聚合关系(has-a),类A聚合类B,表现为类B作为类A的成员变量
  • 继承关系(is-a)

4.3 自定义类

  • 在 java 中对象的拷贝必须使用clone()方法才能获取完整的拷贝

  • this 关键字可以理解为方法的隐式参数,可以区别方法中的局部变量和类的成员变量;也可以理解为是类对象的实例;this关键字还可以作为该类的另一个构造器

  • 假设类中的成员变量含有引用型变量 a,那么 a 的 getter 应该返回 a.clone()。这里涉及到了对象的深拷贝和浅拷贝:

  • 深拷贝:一个对象中引用型成员变量内的属性也拷贝,换句话说深层次的属性成了两份

  • 浅拷贝:仅拷贝引用,换句话说对象中引用型成员变量的属性只有一份


	/*
	 * 一个简单的 A 类有一个简单的属性int attrA,还有操作这个属性的 getter 和 setter
	 * 一个简单的 B 类有两个属性,一个属性是 A a,另一个属性是 int attrB,以及操作这两个属性的 getter 和 setter
	 * 根据封装性,规定当修改一个属性的时候必须通过类的 getter 和 setter 修改,所以当返回一个引用类型的时候,应该返回它的副本
	 * 
	 */

	class A {
	    private int attrA;
	    
	    /* 省略 attrA 的 getter 和 setter */
	    
	    public Object clone() {
		A a = new A(this.attrA);
		return a;
	    }
	}
	
	class B {
	    private A a;
	    private int attrB;
	
	    /* 省略 a 的 setter 和 attrB 的gtter、setter */
	    
	    public A getA() {
		return (A) a.clone();
	    }
	
	
	    public Object clone() {
		B b = new B(this.a, this.attrB);
		return b;
	    }
	
	}
	
	public class hello3 {
	
	    public static void main(String args[]) {
		A testA = new A(1);
	
		B testB = new B(testA, 2);
	
		A myA = testB.getA();
	
		myA.setAttrA(11); // 如果 A 没有 clone方法,就破坏了封装性。因为如果没有调用testB的setA()方法就改变了数据,这是不合法的。
	
		System.out.println(testB.getA().getAttrA()); // output: 1
	
		System.out.println(myA.getAttrA()); // output: 11
	
	    }
	}


  • final 变量也可以更改,例子就是 System 类的 out final 变量有 setOut 方法来改变 out,因为 out 的实现是 native 方法,由其他语言实现的,所以绕过了 java 的存取控制机制。
  • java中的方法参数是值调用。这里要注意区分按引用调用,按值调用,引用型参数类型,值类型参数类型。
  • 从 c语言的角度来分析java对象的传递的情况,其实相当于c语言的指针传递,在函数内部中可以修改实参的指向,而c语言中引用传递是指函数的参数是一个别名。
  • 基于此,java如果要实现swap,那么应该通过一个引用的指向去swap。(内置的IntHolder类就是这么做的)

	int main(void){
		
		int a = 1;
	
		test(a);
	
		printf("out: %d\n", a); // output: 3
	}
	
	void test(int &p){ // p 相当于 a 的一个别名,对形参的更改会引起实参本身的变化
		p = p+2;
		printf("in: %d\n", p); // output: 3
	}
	
	void testPointer(int *p){ // 指针传递,对形参本身的更改不会引起实参的变化,但是对指向内容的更改会引起变化
	}

4.4 类的方法

  • 构造方法:
  • 重载:不仅仅构造方法可以重载,java所有方法都支持重载,注意重载的方法返回类型jdk5之前必须相同,但是现在可以是子类型。
  • 默认构造器:注意,当显示提供了自定义构造器时,不自动生成默认构造器也就不能使用。只有在没有提供任何自定义构造器时候,才会自动生成。
  • this关键字调用另一个构造器

	class A {
	    int a, b;
	    
	    public A(int a) {
		this(a, 2); /// 调用另一个构造器,给另一个构造器提供默认值
	    }
	    
	    public A(int a, int b) {
		this.a = a;
		this.b = b;
	    }
	
	}

  • 初始化代码块:赋值初始化<初始化代码块<构造器初始化。通常可用于静态变量复杂的初始化。
  • package 的命名为什么以 com.how2j.** 形式命名,这是因为 Sun 公司提出建议以域名逆序形式命名,因为域名是唯一的,这样就保证 package 的绝对唯一性。

4.5 高级文档注释

除了基本的//和/**/之外应该掌握文档注释

javadoc对注释生成文档,可以使用标记:@author,@version,@return,@params,@see等进行注释


	public class hello3 {
	    
	    /**
	     * @author 陈
	     * @version 1.0
	     * 
	     */
	    
	    
	    /**
	     * hello 方法的简述
	     * <p> hello 方的详细说明 </p>
	     * @param str 问候语
	     * @param name 名字
	     * @return return 返回格式化字符串
	     */
	    public String hello(String str, String name) {
		return str+"," + name +"!";
	    }
	    
	
	}

  • output

output

5 继承

5.1 超类和子类

  • 置换规则:程序中出现超类的地方,都可以用子类替换;向上转型就是这个意思。

5.2 多态

  • 对象方法的调用过程与动态绑定:

    1. 生成方法表以供动态绑定搜索
    2. 编译器查看声明类型和方法名
    3. 查看参数类型(重载解析)
    4. 如果是private,static,final就直接调用对象的方法,称为静态绑定;其他方法会采用动态绑定,会去查找方法表调用最适合自己的方法,比如自己重写了方法就调用自己的方法,否则的话就去调用父类的方法。

    方法表->方法签名->动态绑定

  • 内联:如果父类方法很短比如,e.getName(),那么虚拟机会优化为 e.name 直接读取,这样就可以加快速度。

5.3 抽象类

  • 抽象方法必须在抽象类中
  • 抽象类中可以有其他方法
  • 抽象类不能有实例,但是可以有抽象变量,该抽象变量只能指向子类的实例。比如:

	// Person 是一个抽象类,Student 是继承 Person 的非抽象类
	Person p = new Student(); // p 是一个抽象变量,指向子类对象

5.4 反射

总的来说就是动态获取类信息,例如在 android 项目开发中,比如有三个Acitivity类,A1,A2,A3,此时运行中的 Activity 可能是 A1 或者 A2,如果 A3 中有两个方法,method1 和 method2,如果是A1->A3,那么执行 method1;如果是 A2->A3,那么执行 method2。一个简单的策略是可以通过Intent进行通信,但是如果有很多 Activity,不同 Activity 要分发不同的 Intent 到 A3 中执行不同的 method,此时通过 Intent 消息通信就比较麻烦。这时就可以利用反射机制,动态的获取当前运行态的 Activity 类的信息分发到 method 中,无疑比 Intent 消息传递来的更加简便。

  • 反射可以拿到 Class 的信息,但是通过 Class 的newInstance()方法无法真正的实例化一个对象,因为无法拿到对象的实例域,方法等。而通过 Field,Method,Constructor可以拿到对象的实例域,方法,构造器。还可以通过 Modifiers 类拿到 public 这样的修饰符。
  • 可以通过 getClass(),Class.forName() 两个方法拿到对象的Class
  • 可以通过 newInstance() 实例化一个 Class 的对象,但是无法通过此对象调用它的方法,实例域等
  • 要想调用方法必须接着 Method 类,首先调用 getMethod()获得Method类获得方法指针,然后调用invoke来使用方法

	...
	try {
	    Class obj = Class.forName(personName); // Class.forName() 根据字符串拿到 class
	    Method method = obj.getMethod(methodName, null); // getMethod() 引用方法

	    method.invoke(obj.newInstance(), null); // invoke 使用实例对象的方法
	} catch (Exception e) {
	    e.printStackTrace();
	}
	...


6 接口与内部类

标记接口是指仅有一个名字,没有内容的接口

6.1 Comparable接口

6.2 Cloneable接口

  • Object默认的是clone()方法是protected的浅拷贝,子类应该实现Cloneable接口,自定义public的深拷贝clone()方法,让任何人都能调用这个方法,克隆这个对象,而不仅仅是子类能调用。

  • 为什么要把Object的clone()方法设置成protected仅有子类能访问?

    1. 这是为了避免任何一个类都具有一个默认的clone()方法,
    2. 也是为了提醒人们必须谨慎的实现clone()方法,因为如果是默认的浅拷贝,那么会导致数据域被修改。

	Class Employee implements Cloneable{
		
		// 提升 clone() 权限
		public Employee clone() throws CloneNotSupportedException{
			return (Employee) supper.clone();
		}
	
	}


6.3 内部类

内部类主要分为普通内部类**(成员内部类)、局部内部类(方法内部类)匿名内部类、嵌套内部类(静态内部类)**。
非静态内部类中不能定义静态成员,静态内部类不能访问外部类普通成员变量(非静态成员)。

  • 成员内部类:
    • 内部类可以访问外部类的所有成员
    • 内部类中的 this 指的是内部类的实例对象本身,如果要用外部类的实例对象: 类名 .this
  • 方法内部类:只能访问方法中的变量,且变量必须式final方法,也可以是final数组
  • 匿名内部类:多用于回调响应,还有lambda表达式。
    • lambda表达式只能取代接口变量

	/*第一段代码:普通的用法*/

	interface A{
	    int add(int a, int b);
	}
	public class hello3 {
	    public static void main(String args[]) {
		A a = (param1, param2)->{return param1+param2;};
		System.out.println(a.add(1, 1)); // output: 2
	    }
	}
	
	/*第二段测试代码:实现runable接口,从下面的代码可以看出runable接口实际上是一个接口变量*/
	...
		new Thread(()->{System.out.println("do something");}).start();
	...

	/*第三段代码:函数参数是接口变量*/
	interface A{
	    int add(int a, int b);
	}
	public class hello3 {
	    public static void main(String args[]) {
		
		int param1 = 1;
		int param2 = 1;
		
		test((int i, int j)->{ // 怎么感觉这么鸡肋
		    return i+j;
		}, param1, param2);
		
	    }
	    // a 是一个接口变量
	    public static void test(A a, int param1, int param2) {
		System.out.println(a.add(param1, param2));
	    }
	
	}

	/*第四段代码: 不用 lambda 版本,用匿名内部类*/
	... 
		test(new A() {
		    @Override
		    public int add(int a, int b) {
			return a+b;
		    }
		}, param1, param2);
	...
	


  • 静态内部类:只能访问外部类静态的东西,自己应该声明为public

6.4 代理

  • 代理的概念:就像通过经纪人去联系明星,经纪人就是代理类。
    • 静态代理:静态创建一个代理类。
    • jdk动态代理:交给jdk代理工厂创建一个新类。 这个新类的特点是:①运行时创建的;②实现了一组指定的接口。
    • 子类动态代理(cglib代理):java默认的代理是目标对象必须实现接口才能被代理,但是有时候要代理没有实现接口的对象,这时候就可以采用cglib代理,cglib和ASM字节码框架有关。
    • javassist动态代理:dubbo使用的这个代理机制,因为它可以动态生成类,所以动态代理就不再话下

①静态代理和目标实现同样的接口,当目标改动的时候,静态代理也要改变。当有多个静态代理的时候,修改起来比较麻烦,所以出现了动态代理。

②感觉动态代理就是要在运行时创建类对象,因为创建了类对象,只能知道它的信息,只能通过Method类获取方法,在类中通过method.invoke()去调用被代理的对象的方法,然后自己在method前后添加逻辑即可。

  • 字节码框架:
    • javassist:javassist是基于反射的,感觉就是通过javassist的api去写一个类和类中的方法
    • ASM:直接操作字节码文件结构的,比较难搞懂

	/*javassist框架实现的代码*/

	import javassist.ClassPool;
	import javassist.CtClass;
	import javassist.CtMethod;
	import javassist.CtNewMethod;
	
	public class MyGenerator {
	    public static void main(String[] args) throws Exception {
	        ClassPool pool = ClassPool.getDefault();
	        // 创建 Programmer 类      
	        CtClass cc= pool.makeClass("com.samples.Programmer");
	        // 定义方法
	        CtMethod method = CtNewMethod.make("public void code(){}", cc);
	        // 插入方法代码
	        method.insertBefore("System.out.println(\"I'm a Programmer,Just Coding.....\");");
	        cc.addMethod(method);
	        // 保存生成的字节码
	        cc.writeFile("d://temp");
	    }

}


output

  • 代理的实现:
    • 静态代理的实现:就是和目标实现统一接口api,访问静态代理达到访问目标的目的
    • 动态代理的实现:依赖于jdk的 newProxyInstance()函数,通过method.invoke()调用父类方法,在invoke()中,可以在被调用的method前后添加逻辑。
    • cglib代理:依赖于ASM字节码框架,生成子类,在子类中通过方法拦截器methodIntercept()调用父类方法,可以重写intercept在被抵用的method前后添加逻辑

	/*jdk代理核心api*/
	static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h )


10 部署应用程序和applet

10.1 JAR包

  • JAR:java归档,传统的是使用zip格式对图片,文本等压缩打包在一起,javaSE5之后使用pack200压缩技术打包,新的技术压缩率接近百分之90
  • jar命令打包:jar options retFileName targetFile1 tartgetFile2

	/* client input: jar options retFileName targetFile1 tartgetFile2 */

	>> jar cvf first.jar picture.png


  • 可运行的jar包:这个不太会,暂时略过。
  • jar密封:其他类不被package命令加载进来,也暂时略过。

12 泛型程序设计

要记住几个事实:类型消除、限定类型、自动强制转换、桥方法

  1. 虚拟机中没有泛型,只有普通的类和方法;(类型消除)

  2. 所有的类型参数(T,U等)都用它们的限定类型替换;(限定类型)

  3. 为保持类型安全性,会加入自动强制转换(自动强制转换)

  4. 桥方法是因为子类方法未覆盖住父类方法,多态灾难;(桥方法)

12.1 简单泛型类


	...
	class<T, B, C, D> test{ // 可以有多个类型变量
	...
	}
	

12.2 简单泛型方法

	
	// 注意类的T和泛型方法的T没关系
	class Test<T>{
		...
		public static <T extends Father> T genericMethod(T a){
			return a;
		}
	
		...
	}


12.3 类型变量的限定

  • 对类型变量 T 进行限定,比如限定这个 T 类型必须实现了 MyInterface 接口,
  • 只能用 extends 关键字,
  • 多个限定类用 & 连接

	interface MyInterface1{ 
		...
	}
	
	class MyClass implements MyInterface1, MyInterface2{
		...
	}

	// 这里的 T 只能是 MyClass 类
	class FirstGeneric<T extends MyInterface1 & MyInterface2>{
		...
	}


12.4 类型擦除

  • 类型擦除,原始类型,限定类型:
    • 泛型会发生类型擦除,然后变为原始类型。
    • 默认T 的原始类型是Object,如果T有限定,那么原始类型就是限定类型。
    • 在泛型中能不能调用原始类型的method,取决于原始类型有没有这个方法。比如原始类型如果是默认类型,就会调用失败:【T.method() <=> Object.method(),因为类型擦除为 Object,而Object中没有method(),所以失败了】
    • 要想成功调用泛型方法,要满足两个条件:①具体类是限定类子类;②限定类实现了method()。

实际上类型擦除后,程序员是否正确使用泛型类,在编译前,IDE 就会自动检查,真方便

	
	class Test<T>{ // 类型擦除为原始类型
		T attr;
	}

	/* <=> */

	
	class Test{
		Object attr; // 默认为 Object
	}
	

	
	class Test<T extends Father & Mother>{ // 类型擦除为离得近的限定类型 Father,而mother不管她
		T attr;
	}

	/* <=> */

	class Test{
		Father attr; // 此时是Father
	}


	class A{ // 单独的具体类
		public void noMethod(){
			System.out.println("run noMethod");
		}
	}

	class Father{  // 泛型限定父类
		public void method(){
			System.out.println("run FatherMethod");
		}

	}

	class Child extends Father{ // 继承了限定类的具体类
		public void childMethod(){
			System.out.println("run ChildMethod");
		}
	}


	class Test<T extends Father>{	// 泛型类
		public void testMethod(T t){ 
			t.method();	// 调用父类已经实现的方法,并且具体类是父类的子类才会成功
		}

		public void testNoMethod(T t){
			t.noMethod(); // 因为父类没有实现 noMethod(),所以即使传进来的 T 类已经实现了 noMethod() , 依然会报错
		}

		public void testChildMethod(T t){
			t.childMethod();  // 父类没有实现 childMethod
		}
	}

	public class hello{ // 主类进行测试工作
		public static void main(String args[]){

			Test<A> t = new Test<>();
			
			A a = new A(); // 单独的具体类
			t.testMethod(a); // 失败,限定类实现,但是具体类A非子类
			t.testNoMethod(a); // 失败,限定类未实现,且具体类A非子类
			
			Test<Child> childT = new Test<>(); 

			Child child = new Child(); // 限定类子类

			childT.testMethod(child); // 成功,满足两个条件
			childT.testChildMethod(child); // 失败,限定类未实现
			
		}
	}




12.5 翻译泛型表达式

  • 编译器自动强制转换:就是调用getter的时候,会先转换为原始类型,然后再自动强制转换。

	class Test<T>{
		private T attr;
	
		public Test(T attr){ // 构造器
			this.attr = attr;
		}
	
		public T getAttr(){ // getter 方法返回一个 T 类型
			return this.attr;
		}
	
	}
	
	public class hello{
	
		public static void main(String args[]){
	
			Test<Integer> test = new Test<>(123);
			
			Integer myInt = test.getAttr(); // <=> test.getAttr()先返回Object,然后Integer强制转换: Integer myInt = (Integer) test.getAttr();
	
		}
	}


  • 编译器的构造方法:应该构造方法就显示指明泛型类,否则会出现下述 par1 的现象,即编译器发生类型擦除后默认是Object类,然后jvm自动强制转换将Object转换为String,所以 getSecond() 应该返回 String,实际上却是Integer。

	class Pair<T>{  
	       private T first=null;  
	       private T second=null;  
	  
	       public Pair(T fir,T sec){  
	            this.first=fir;  
	        	this.second=sec;  
	       }  
	       public T getFirst(){  
	             return this.first;  
	       }  
	       public T getSecond(){  
	             return this.second;  
	       }  
	       public void setFirst(T fir){  
	         this.first=fir;  
	       }  
	}

	...

		Pair<String> pair1=new Pair("string",1);     // 构造出来没问题,但是在调用 getSecond()出错
    	Pair<String> pair2=new Pair<String>("string",1)    // 编译器直接出错

	...

12.6 桥方法

  • 由于继承泛型的时候,子类没有覆盖住父类的方法,导致多态出现了问题:下面代码中,程序员的本意是想要用子类setter去覆盖父类setter,结果由于类型擦除,父类的setter成了 setFirst(Object fir),因此子类中出现了两个 setter,编译器的解决办法就是桥方法。
  • 桥方法:继承的父类setter中调用子类的setter。
  • 桥方法在 getter 的时候会很神奇:虽然jvm不允许程序员编写方法签名一样的方法,但是它自己可以生成方法签名一样的方法。因为jvm是根据返回值和方法签名唯一确定一个方法的。

	class Pair<T>{
		public void setFirst(T fir){...}

		public T getFirst(){...};
	}

	// 讨论 setter
	class SonPair extends Pair<String>{  

		public void setFirst(String fir){...}  // 此时由于类型擦除现象会有两个 setter,无法覆盖父类 setter
		
		
		public void setFirst(Object fir){ // jvm编译期间自动生成桥方法,程序员不可见
			setFirst((String) fir) // 子类的setter,且强制转换参数
		}
	}  

	// 讨论 getter
	class DaughterPair extends Pair<String>{

		public String getFirst(){...}; // 仍然有两个 getter

		public Object getFirst(){...}// 会出现神奇的事情,桥方法会出现签名一样的方法,虽然程序员不能编写签名一样的方法,但是jvm是通过返回值和方法签名唯一确定一个方法,所以它自己可以生成这种签名一样的方法
		
	}

	
	




12.7 使用泛型时的约束和局限性总结

  • 泛型类中,不能在静态域,静态方法中引用泛型变量(如果是泛型方法,允许使用)

	class Pair<T>{
		...
	
		private static T a ; // 禁止
	
		private static T get(){...}; // 禁止
	
		private static set(T t){...}; // 禁止

	
		...
	}

  • 禁止使用泛型数组

	...

	Pair<String>[] stringPairs=new Pair<String>[10]; // 禁止

	...


  • 禁止使用 new T(…) 这样的表达式,可以通过Class类本身也是泛型Class t,然后利用反射t.newInstance()方法来实现功能。
	...

		public static <T> Pair<T> makePair(Class<T> cl){
			try{
				return new Pair<T>(cl.newInstance(), cl.newInstance());
			}catch(Exception e){
				return null;
			}
		}

		/* 
		等价于下面这种写法,但是下面这种写法不支持
		<=> 
		public Pair(){ 
				fir = new T(); 
				sec = new T()
		}; 
		*/
	...

	...
	/* 调用方法 */
		Pair<String>  p = Pair.makePair(String.class);
	
	...



12.8 泛型之间的继承问题

如下问题 Test 不是 Test 的子类,这就引出了通配符问题。


	class Father{
		...
	}

	class Son extends Father{
		...
	}

	class Test<T>{
		...
	}

	...

	Test<Son> 不是 Test<Father> 的子类

	...

12.9 通配符类型编程(注意它和泛型编程的区别)

感觉书上的写的看着好累,但是确实经典,但是解释不全面

参考博客1,总结得很到位

参考博客2,讲解得很仔细

参考博客3,讲到了协变,逆变,不变的概念

参考博客4,解释的很完美啊,和自己的理解也特别符合,和我的用例也相当符合


  • 协变,逆变,不变:
    • 协变:当A≤B时有f(A)≤f(B)成立
    • 逆变:当A≤B时有f(B)≤f(A)成立
    • 不变:当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系

  • 泛型方法与类型通配符的区别:
    • 通配符方法比泛型方法更加通用
    • 泛型是不变的,即使它的类型参数存在继承关系,但是整个泛型之间没有继承关系 : ArrayList list = new ArrayList(); -> Error
    • 在java泛型中,引入了 ?(通配符)符号来支持协变和逆变,解决了泛型的继承问题

	/* 
	 * 下面代码中Child1和Child2是Father的儿子,A是泛型类
	 * Test类中有两个method:method1测试泛型方法,method2测试通配符方法
	 * 可以看见method1中,只能是Child1,而method2中还可以其他孩子
	 * 因此通配符方法比泛型方法更加通用
	*/

	class Father {
	
	}
	
	class Child1 extends Father{ 
	    
	}
	
	class Child2 extends Father{
	    
	}
	
	class A<T>{ // 泛型类
	    
	}
	
	class Test{  // 测试类
	    public void method1(A<Child1> a) { // 泛型方法,只能是Child1
			System.out.println("run method1");
	    }
	    
	    public void method2(A<? extends Father> a) { // 通配符方法,可以是Father的所有孩子
			System.out.println("run method2");
	    }
	}
	
	
	
	
	public class hello2 { // 主类进行测试工作
	
	    public static void main(String args[]) {
	
		A<Child1> aChild1 = new A<Child1>();
		
		A<Child2> aChild2 = new A<Child2>();
		
		Test test = new Test();
		
		test.method1(aChild1);
		//test.method1(aChild2): // 其他孩子就报错了
		    
		test.method2(aChild1);
		test.method2(aChild2);
	    }
	}


  • 子类型限定 <? extends Father>:“in” 类型生产者,可以用getter提供返回值,但是无法使用setter消费

  • 超类型限定 <? supper T>:“out"类型消费者,可以用setter消费值,但是无法使用getter生产

	
	/* 无法使用setter和getter的解释 */


	class Father {
	
	}
	
	class Child1 extends Father{ 
	    
	}
	
	class Child2 extends Father{
	    
	}
	
	class GrandChild extends Child1{ // Father的孙子,Child1的儿子
	    
	}
	
	class GrandGrandChild extends GrandChild{ // 曾孙子
	    
	}
	
	class A<T>{ // 泛型类
	    private T t;
	    public A(T t) {
		this.t = t;
	    }
	    
	    public T get() {
		return t;
	    }
	    
	    public void set(T t) {
		this.t = t;
	    }
	    
	}
	
	class Test{  // 测试类
	    
	}
	
	
	
	
	public class hello2 { // 主类进行测试工作
	
	    public static void main(String args[]) {
		Father father = new Father();
		Child1 child1 = new Child1();
		Child2 child2 = new Child2();
		GrandChild grandChild = new GrandChild();
		
		A<Father> aFather = new A<Father>(father);
		A<Child1> aChild1 = new A<Child1>(child1);  
		A<Child2> aChild2 = new A<Child2>(child2);
		A<GrandChild> aGrandChild = new A<GrandChild>(grandChild);
		
		
		
		A<? extends Father> aChild = aChild1; // ? 是子孙,它的 getter 没问题,但是 setter 报错
		
		/*
		 * getter:? extends Father get(){...}
		 * setter: void set(? extends Father){...}
		 */
		
		Father fRet = aChild.get(); // getter 返回一个不确定的子类对象,编译器用capture#1占位符来代替这个不确定的子类,具体是什么不知道,只能用父类引用去接它
		
		// aChild.set(child2); // 报错,因为必须是 capture#1 类型才允许插入,但是Father的子类无法匹配这个类型,所以就直接插不进去
		
		
		A<? super GrandChild> bGrand = aGrandChild; // ? 是祖先,它的  getter 报错,setter 没问题
		
		/*
		 * getter: ? super GrandChild get(){...}
		 * setter: void set(? super GrandChild){...}
		 * 
		 */
		
		// Object obj = bGrand.get(); // 返回的是一个 capture#1 占位符父类对象,没有东西可以接它
		
		bGrand.set(grandChild); // 参数是grandChild的所有父类引用占位符capture#1,所以设置子类没问题
	
		bGrand.set(new GrandGrandChild()); // 设置了一个子孙对象
	    }
	}


  • 无限定通配符<?>:<? extends Object>,getter只能返回给Object,而setter不能使用

  • PECS 原则:producer extends consumer supper。注意:<? extends Father>,jvm底层理解为占位符capture#2,getter时候返回匹配的所有下界对象,只能用上面的父类去引用这个对象;而<? super Child>,也是理解为占位符,setter的时候匹配所有上界引用,只能用子类对象给它。总之,记住一句话,父类可以引用子类对象,子类不能引用父类对象。

  • 通配符的捕获:因为通配符表示一个类型的范围,当通配符在运行的时候是一个确定的类型的时候,就可以用helper类捕获。





  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

From_CQUPT

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值