###Java面向对象部分笔记
(对象和类 抽象与封装 继承与多态)
面向对象三大支柱是: 封装 继承 多态
0 面向对象5大原则
单一职责原则(Single-Resposibility Principle):一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。
开放封闭原则(Open-Closed principle):软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。
Liskov替换原则(Liskov-Substituion Principle):子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。
依赖倒置原则(Dependecy-Inversion Principle):依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
接口隔离原则(Interface-Segregation Principle):使用多个小的专门的接口,而不要使用一个大的总接口
1 对象和类
(1)对象和类的基本概念
类是对象的模板,一个类的声明中定义了对象应该有的变量、方法以及构造实例的构造方法。通过声明变量和构造方法可以创建类的实例(类名要大写)
类是一种引用类型,区别于8种基本数据类型,所有变量存储的都是都是对类的一个实例的地址客空间的地址。(注意 变量的声明 对象的创建 以及 对象引用的赋值 这三个过程的区别)
通过 “.” 来访问对象的变量和方法
实例变量 实例方法(与创建的对象绑定)
*静态变量(所有类的实例共享该变量,在公共内存地址,无需创建对象就可以访问) *
静态方法(可以通过类直接调用,无需创建对象)
(也可以通过引用变量来调用)(要加修饰符 static)
常量 :类中的常量是被所有对象共享的,应当被声明为final static
(实例方法可以调用其对象所有资源,而静态方法只能调用类的静态资源)
匿名对象(直接new了用,不给变量指向它)
声明一个引用对象而不赋值,Java不会给其赋默认值null
如果一个对象的数据域中的引用类型的变量没有引用对象,Java会给其赋默认值null(基本类型会赋 0 /u000 false),但Java不会给方法中的局部变量(包括基本类型)赋默认值
每个变量都代表一个内存地址,基本类型变量指向的内存地址存的是值,而引用类型变量存的是指向是这个对象实际内存地址
不被任何变量指向的对象会被JVM回收掉(垃圾回收)
含有main方法的是会被执行的类,成为主类(main函数是程序程序的入口)其他类都不可运行
一个文件可以有多个类,但只能有一个public类,只有这个public类中才会有main函数,公共类必须和文件同名。(但注意编译的时候每个类都会编译成一个 .class文件)
(2)修饰符
(2.1)可访问性修饰符(四个等级)
(用于确定一个类及其成员的可见性)
package用来组织类,表明类的归属
不添加修饰符被视为可以被同一个package的类访问
public表示可以被任何类访问
private表示只能在它自己的类中被访问(只能用于类的成员上)
如果一个类不是public的,则它只能被同一个package的类访问
构造方法可以是public也可以是private(仅当时静态类不想被创建实例时使用,一般都是公共的)
同一个类中 | 同一个package中 | 在子类中 | 在不同package中 | |
---|---|---|---|---|
public | O | O | O | O |
protected | O | O | O | X |
(default) | O | O | X | X |
private | O | X | X | X |
protected允许子类可以访问父类的变量,而默认(default)和private不可以
private和protected只可以用于类的变量和方法,而public和默认(default)可以用于类或者类的变量和方法
注意方法的修饰的类型,要在子类中调用或者重写的方法一定要在父类声明时用public或者protected(子类可能不在同一个package内!)
(2.2)防止扩展与重写(final修饰符)
final修饰的类不可以被继承,final修饰的方法不可以被子类重写,final还可以修饰局部变量,这时这个局部变量变量就是常量
(3) 数据域封装
将类的所有变量都设为私有变量,然后通过公共的getter和setter方法进行访问
(4) 调用类的方法的底层细节
基本类型的变量存放在栈中,引用类型的对象被创建后会存放在内存的堆中。当方法被调用时其局部变量被放在栈中。栈中引用类型的变量存的是指向栈中对象的引用。
(待补充)
(5) 不可改变类和对象
通过不可改变类创建的对象就是不可改变对象,比如String
如果一个类是不可改变类,那么其 1数据域必须是私有的,2不能提供setter方法,3没有一个返回指向可变数据域的引用的getter
(6) 局部变量优先于类变量(类变量被隐藏)
(7) this引用
this指向调用对象自身,也可以在构造方法内部用于调用同一个类的其他构造方法(this(其他构造方法的参数列表) 要求这个在构造方法中出现在任何其他可执行语句之前)
当类变量被隐藏时,必须用this来引用
(8) 类的抽象和封装
抽象是指将类的使用和实现分离
封装是指实现的细节被封装并且对用户隐藏
类被称作 抽象数据类型ADT
(9) 类的关系
关联 聚集(has-a 所有者称为聚集对象) 组合(一个对象只归属于一个聚集对象) 继承
(10) 基本类型和包装类型
既可以用数值也可用字符串来构造包装类 new Integer(“7”)
每个包装类都包含 类似 intValue() 的方法进行拆箱
包装类没有无参构造方法,所有包装类的实例都是不可变的
每一个数值包装类都是常量MAX_VALUE和MIN_VALUE,代表基本数据类型的最大最小值(对于·浮点代表最小正值)
都有compareTo方法来进行比较(也可以直接用关系操作符比较)
静态方法 Integer.valueOf(String s)
创建一个新的包装类的对象(将String转化为包装类)
静态方法 Integer.parseInt(String s , int radix)
将String转化为一个十进制基本数据类型(后一个参数是说明这个String中的数是几进制,可以没有)
装箱 开箱 Java允许基本类型和包装类的自动转换,编译器会根据需要自动装箱开箱
(11) BigInteger BigDecimal (去看一下源码是怎么实现的)
new BigInteger(String) 来创建
BigInteger的实例可以是任意大小整数 BigDecimal 没有精读限制,但可以用重载的方法限制来避免除不尽的异常
使用 add subtract multiple divide remainder 来完成算术运算 compareTo来比较
都是不可改变的
(12) String
Java将字符串直接量看做String对象
也可以用char[] 来创造一个字符串
字符串转换成字符数组 toCharArray()
使用format方法格式化字符串
关于new和直接赋值的不同,以及在==下情况的不同
直接赋值而不是使用new关键字给字符串初始化,在编译时就将String对象放进字符串常量池中;使用new关键字初始化字符串时,是在堆栈区存放变量名和内容;字符串的拼接操作在程序运行时,才在堆中创建对象。一般,可以认为使用==”比较的是引用,equals比较的是内容。
String str1 = “java”; //直接赋值而不是使用new关键字给字符串初始化,在编译时就将String对象放进字符串常量池中
String str2 = new String(“java”);//在堆中创建新对象
String str3 = “java”; //直接赋值而不是使用new关键字给字符串初始化,在编译时就将String对象放进字符串常量池中
String s = str1+str2; //字符串的拼接操作在程序运行时,才在堆中创建对象,相当于new
System.out.print(s==”javajava” s1==s3 s1==s2);
答案是 false true false
(13) 正则表达式 使用 字符串.matches(“正则表达式”) 来匹配符合正则表达式的字符串(返回true false)(学习一下正则表达式的规则)
(14) StringBuilder StringBuffer(字符串构造器)(去看一下JDK源码)
StringBuilder 是同步的,只有一个任务被允许执行方法;StringBuffer多任务并发访问的
除了这一点他们的方法都是一样的(单任务Builder更有效)
默认构造容量是16,可以指定容量,指定字符串(3个构造方法)
(如果有用到就去看看其方法如何使用)
2 继承(is-a)(class Apple extends Fruit )
Java不允许多重继承,但是可以通过接口来实现
定义一个通用的类(父类),之后扩充该类为一个更加特定的类(子类)
子类拥有父类的所有属性和方法(父类的构造方法不会被继承),共有属性和方法可以直接使用,但父类的私有属性和方法子类是无法直接使用的,私有方法是用不了的,而私有属性则必须父类提供了get或set才可以访问或修改
分析内存后,会发现,当一个子类被实例化的时候,默认会先调用父类的构造方法对父类进行初始化,即在内存中创建一个父类对象,然后再父类对象的外部放上子类独有的属性,两者合起来成为一个子类的对象
super关键字 指代父类 可以用于调用父类中的普通方法和构造方法
在子类的构造方法中显示的super必须在第一条语句,不可以直接写父类名字(注意即使你不显式的写super,编译器也会自动的将super()作为第一条语句,先调用父类的构造方法)
构造方法链: 当构造一个类的实例时,会调用沿着继承链的所有父类的构造方法(从祖先开始,一层一层调用创建)(所以设计父类的时候最好提供一个无参构造方法,防止子类的构造方法调用时出现错误)
方法重写(覆盖)(overriding):当父类中的一些方法对子类不适用时,就需要提供一个新的方法来 override 父类中的这个方法。(注意 私有方法和静态方法不可被重写,静态方法一旦被重写,父类中的就会被隐藏,要通过 父类名.方法名 调用)
*遵循 “两同两小一大”原则 *
两同:方法名、参数列表相同。
两小:子类的返回类型(指的是继承关系的大小)、抛出的异常小于等于父类
一大:访问修饰符大于等于父类的访问修饰符(四个等级)
当被重写时,就可以使用super调用父类的原方法
在编写代码时,建议在覆盖一个方法时加上 @override,除了标识的作用,它还会帮助你检查你覆盖的这个方法是否在父类中真的存在。
方法重载(overloading):多个方法具有相同的名字且拥有不同的参数列表时,就会出现重载,由编译器决定调用哪个方法,这个过程被称为 overloading resolution(重载解析)
方法重载既可以在统一各类中发生,也可以在子类中重载由父类继承而来的方法
Java中所有的类都继承自Object类(Java.lang.Object lang里的东西都不用import (编译器会自己带上))如果一个类没有指定父类,那其父类就默认是Object(所以除了Object所有类都有有父类,都存在继承)
Object的 toString()方法会返回一个描述该对象的字符串,类名+@+该对象16进制的内存地址,我们可以对它进行重写
3 多态
多态意味着父类的变量可以指向子类对象
面向对象三大支柱是: 封装 继承 多态
子类型 父类型d
每个子类的实例都是父类的实例,但是父类的实例不是子类的实例
所以总可以将子类的实例传给父类的变量,但反过来不行
(多态意味着父类型的变量可以引用子类型的对象)
声明类型&实际类型:对象声明时的类型是声明类型,而实际类型是其声明类型或者声明类型的子类,要看具体指向什么。(声明类型决定了编译时匹配哪个方法)
(运行时)动态绑定:由对象的实际类型决定调用哪个方法(是父类的还是子类重写的)(方法可以沿着继承链的多个类中实现,JVM决定运行时调用哪个方法)(先看实际类型,没有的话会从那个子类往祖先找实现的方法?)
对象转换和instanceof运算符
对象转换:对象的引用可以类型转换为对另外一种对象的引用
隐式转换:声明是父类,而赋值是子类的引用(注意此时的类型是父类,不能直接赋给子类的变量,需要显示转换成子类) Object o = new Student();
*显式转换:Student b = (Student) o;
*
向上转换:总是可以将子类变量转换为父类变量,子类的实例永远是它父类的实例,可以隐式转换(直接赋值就行)
向下转换:将父类实例转换为子类变量,必须要显式转换(转换的前提是这个父类的实例本来就是子类的实例,只是声明是父类)
*instanceof操作符(是关键字):用于判断一个对象obj是否为一个类的Class 的对象,或者是其直接或间接子类,或者是其接口的实现类(是则返回true) if(obj instanceof Class)
*
Java关键字中每个字母都是小写的
对象转换的好处:将变量定义为父类型的好处是可以接受任何子类型的值,在需要使用的是时候再具体转换,方便管理使用
注意!成员访问符 . 的优先级高于类型转换,要加括号!((Student) o ).toString()
Object类的equals方法:A.equals(B) 判断两个对象是否一样 ==实现(指向同一对象),在String和Date都被重写,用于比较内容(而不是直接==)
4 抽象类
抽象类不可以创建对象,可以包含抽象方法(可以有普通成员),这些方法将在子类中实现。
抽象类和抽象方法用abstract来修饰,抽象方法只有方法头的定义,不能有方法体( abstract void time(参数);(注意这些在子类都不能改))
抽象方法不能包含在非抽象类中;抽象类可以有构造方法不能new但是可以给子类调用(所以要声明为protected);子类可以覆盖父类的方法并定义为abstract(在父类方法于子类中无效时),但子类必须声明为为abstract;父类具体子类也可以抽象;抽象类可以作为声明类型(虽然不能new);子类可以不完全实现抽象方法(可以不用重复写在子类中),但这样子类也必须是抽象类
5 接口
父类型了相关子类中的共同行为,接口可以用于定义类的共同行为(包括非相关的类);接口之只包含常量和抽象方法
接口目的在于知名相关或者或者不相关类的多个对象的共同行为
Java中接口被看做特殊的类,每个接口和类一样被便以为独立的字节码文件
(1)接口的声明和使用
声明一个接口:修饰符 interface 接口名{ 常量声明 抽象方法 }
利用extends 接口可以继承其它接口(可以继承多个(相当于它的父类))(称为子接口)
类和接口的关系称为 接口继承 类使用implements来继承接口(和类继承一样)
和抽象类类似,接口不能new,但可以作为数据类型。一个接口的变量可以引用任何实现该接口的类的实例,一旦一个类实现了该接口,这个接口(以及这个接口继承的所有接口 都)相当于它的父类。(显式、隐式转换同样适用)
(2)接口的变量和方法(变量是公共静态的,方法是公共抽象的)
注意!接口中所有数据域都是 public static final 所有方法都是 public abstract 所以Java允许在写接口时忽略这些修饰符(所以继承自接口的方法必须是公共 抽象 实例方法(不允许静态方法))
注意!一个类必须实现它继承接口的所有抽象方法!
(3)接口的静态方法
对于静态方法,以往通常的做法都是将静态方法放在伴随类中。在标准库中,你会看到成对出现的接口和实用工具类, 如 Collection/Collections 或 Path/Paths。
而在 Java SE 8 中,允许在接口中增加静态方法。(所以可以直接写静态方法,伴随类已经过时了)
(4)接口的默认方法
对于接口的方法可以提供一个默认实现,用default
标记。大部分时间没什么用(因为一单继承就要被覆盖)
默认方法的一个重要用法是“‘ 接口演化” (interface evolution),因为为接口增加一个非默认方法不能保证“源代码兼容”,所以对于老接口进行新增时往往用默认方法
比如说我们之前类A实现了接口B,现在我们在接口B又新添加了一个方法C,这时候如果C不是default的,那就会出现编译错误(因为A没有实现C)
不过, 假设不重新编译这个类, 而只是使用原先的一个包含这个类的 JAR 文件。这个类 仍能正常加载, 尽管没有这个新方法。 程序仍然可以正常构造 Bag 实例,不会有意外发生。(为接口增加方法可以保证“ 二进制兼容”)。不过,如果程序在一个Bag实例上调用stream 方法, 就会出现一个 AbstractMethodError。
将方法实现为一个默认方法就可以解决这两个问题。 Bag 类又能正常编译了。 另外如果 没有重新编译而直接加载这个类, 并在一个 Bag 实例上调用 stream 方法, 将调用 Collection. stream 方法。
默认方法冲突(这里有很多情况,具体等遇到再仔细看吧)
如果先在一个接口中将一个方法定义为默认方法, 然后又在超类或另一个接口中定义了 同样的方法就会产生冲突,Java用以下规则解决这个问题:
1 ) 超类优先。 如果超类提供了一个具体方法, 同名而且有相同参数类型的默认方法会被忽略。
2 ) 接口冲突。 如果一个超接口提供了一个默认方法, 另一个接口提供了一个同名而且 参数类型(不论是否是默认参数)相同的方法, 必须覆盖这个方法来解决冲突。
(5)一些实用的接口
- Comparable接口:定义了compareTo,用于比较
public interface Comarable<E> {
public int compareTo(E o);
}
如果自定义的类也要实现对象间比较,要继承并实现这个接口(自定义比较逻辑)
这是一个泛型接口,所有包装类和Date都实现了这个接口,所以他们之间是可以比较大小的
- Cloneable接口:定义了clone,用于创建一个对象拷贝
public interface Cloneable{ }
一个空的接口称为标记接口,及不含常量也不含方法,用来表示一个类拥有某些特定属性
实现Cloneable接口的类标记为可克隆的,并且它的对象可以使用在Object类中定义的clone()方法克隆
Object类中对clone()方法头:
protected native Object clone() throws CloneNotSupportedException
native表示不是用Java写的,但它是JAM针对自身平台来实现的
6 Java 复制
Java中有三种复制方式:直接赋值复制、浅复制、深复制
1 直接赋值复制
基本类型复制的是值,对象复制的是引用
后两种方式都是基于Object中的clone()方法来复制的,它的原理是创建一个新的Object对象,然后把所有成员都按照直接赋值复制方式复制一份
2 浅复制
重写Object中的clone()方法,克隆一个对象,但是数据域中的抽象成员复制的是引用
要在目标类上implements Cloneable接口,然后重写这个方法(改成public)
public Object clone() throws CloneNotSupportedException{
return super.clone();
}
在使用clone()方法时注意要显式转换一下(因为声明类型是Object的)
House house2 = (House)house1.clone()
这里要注意,Object中的clone()方法是将原始对象的每个数据域复制给目标,基本类型直接赋值,而抽象类型复制的是引用
另一种代码:
class Resume implements Cloneable{
public Object clone() {
try {
return (Resume)super.clone();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
3 深复制(两种实现方式)
深复制为包括数据域的对象在内的全部克隆(其实就是把对象成员再用clone给克隆了再赋值)(克隆的范围扩大到类的成员)
但这里有一个问题,如果成员的成员也是对象,那样第三层将复制的还是引用,有没有办法直接解决这个问题?
1 利用Object中的clone()方法实现代码:
class Student implements Cloneable { String name;
int age;
Professor p;
Student(String name, int age, Professor p) { this.name = name;
this.age = age;
this.p = p;
}
public Object clone() {
Student o = null;
try {
o = (Student) super.clone();
} catch (CloneNotSupportedException e) {
System.out.println(e.toString());
}
o.p = (Professor) p.clone();
return o;
}
}
2 利用序列化实现深复制:
在 Java 语言里深复制一个对象,常常可以先使对象实现 Serializable 接口,然后把对
象(实际上只是对象的一个拷贝)写到一个流里,再从流里读出来,便可以重建对象。
7 泛型
泛型可以使我们在编译时而不是运行时检测出错误
泛型可以将类型参数化,使我们可以定义带泛型类型的类或方法,随后编译器会用具体的类型来替换它。
泛型类型和或方法允许用户指定和这些类和方法一起工作的对象类型,如果试图使用一个不相容的对象,编译器就会检测出这个错误。
从JDK1.5开始,Java允许定义泛型 类、接口、方法
<T>或<E>
表示形式泛型类型,随后可以用一个实际具体类型来替换他。替换的过程称为 泛型实例化
泛型类型只能是引用类型
注意!在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
受限的泛型类型(确定泛型的上下边界):将泛型定义为<E extends 类A>
表明泛型E必须是A的子类型(泛型的上下边界添加,必须与泛型的声明在一起)
泛型类和接口
可以定义泛型接口和泛型类(虽然将元素类型设置为Object也可以容纳所有对象,但不能编译时检测出错误)
带泛型类型的类的构造方法还是和没有的时候一样
不能对确切的泛型类型使用instanceof操作
带泛型的类或者接口,其类和接口名就是带<E>
的,表示了它里面使用了泛型E。对于类来说,在new这个类的实例的时候或者声明这个类的实例,再表明实际类型。对于接口来说,在类继承此接口时,要么在类继承时标明实际类型,要么类也一起加上泛型
public class Stack<E1,E2,E3> {}
Stack<String,Integer,Double> stack = new Stack<>();
public interface Comparable<E> {
public int compareTo(T o);
}
public interface Generator<T> {
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;
}
}
/**
* 当实现泛型接口时确定泛型实际类型时,类名就不用再加<>
*/
class MeetGenerator implements Generator<String>{
@Override
public String next() {
return null;
}
}
泛型方法
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
/**
* 泛型方法的基本介绍
* @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;
}
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
//虽然在方法中使用了泛型,但是这并不是一个泛型方法。
//这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
//所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey(){
return key;
}
}
/**
* 这才是一个真正的泛型方法。
* 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置.
* 泛型的数量也可以为任意多个
*/
public <T,K> K showKeyName(Generic<T> container){ }
//当在泛型类中定义泛型方法
class GenerateTest<T>{
public void show_1(T t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
//由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
public <E> void show_3(E t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
public <T> void show_2(T t){
System.out.println(t.toString());
}
}
注意!如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法。(否则非法)
public class StaticGenerator<T> {
/**
* 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
* 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
* 如:public static void show(T t){..},此时编译器会提示错误信息:
"StaticGenerator cannot be refrenced from static context"
*/
public static <T> void show(T t){
}
}
泛型通配符(通配泛型)
Ingeter
是Number
的一个子类,Generic<Ingeter>
与Generic<Number>
实际上也是相同的一种基本类型。但是在使用Generic<Number>
作为形参的方法中,不能使用Generic<Ingeter>
的实例传入。Generic<Integer>
不能被看作为``Generic
所以需要一个在逻辑上可以表示同时是Generic<Integer>
和Generic<Number>
父类的引用类型。由此类型通配符应运而生。
FruitGenerator<?> t = new FruitGenerator<>();
类型通配符一般是使用?代替具体的类型实参,注意了,此处?
是类型实参,而不是类型形参 。此处的?
和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类,是一种真实的类型。(不能用于类和方法要使用的泛型的声明,是用在调用和使用上)
可以解决当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。
有三种形式的通配泛型,<?>
表示任意类型 <? extends T>
表示是T或T的一个子类型 <? super T>
表示是T或T的一个父类型 (其中T是泛型类型)
public void showKeyValue1(Generic<? extends Number> obj){
}
public <T> void showKeyValue1(Generic<? extends T> obj){
}
具体关系看一下P12的那个图就会清晰了
原始类型和向后兼容
没有指定具体类型的泛型类称为原始类型(相当于指定的类型是Object,ArrayList相当于ArrayList
消除泛型(编译成字节码后就没有泛型了)
泛型是使用类型消除的方式实现的,编译器使用泛型类型信息来编译代码,但随后就会消除它,所以泛型信息在运行时是不可用的(这种方法可以是泛型代码向后兼容使用原始类型的遗留代码)
一旦编译器确定泛型类型是安全使用的,就会把它转变为原始类型。如果是非受限的,就会使用Object来进行代替;如果是受限的,就会用该受限类型进行替换。
ArrayList<String> list1 = new ArrayList<>();
String a = list1.get(0)//会变为下面语句
ArrayList list1 = new ArrayList();
String a = (String)list1.get(0)//要做类型转换(因为现在里面的元素的声明类型都是Object的,所以要显式转换一下)
注意!不管实际的类型是什么,泛型类被它的所有实例所共享
ArrayList<String> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();
list1 instanceof ArrayList //true
list2 instanceof ArrayList //true
list1 instanceof ArrayList<String> //会报错,没有这个类型
尽管编译时有两种类型,但是在运行时只有一个一个ArrayList类会被加载到JVM中,list1和list2都是ArrayList的实例,但是ArrayList<String>
这个类型在运行时是不存在的(不是单独一个类),所以上述instanceof才有以上结果
对泛型的限制(由于运行时(编译完成后)泛型被消除)
不能使用 new E()
可以声明一个泛型类型的数组变量,但不能创建一个确切的泛型类型的数组,不能使用 new E[] ,也不能用泛型类创建数组
ArrayList<String>[] list = new ArrayList<String>[10]
是错误的(1)可以通过new一个该泛型类的原始类型的数组,然后做一个显式的类型转化(不在这里做的话,就要在取数据的时候要做类型转换)(会有编译免检警告,无法确保运行时类型转换成功)
(2)使用通配符进行数组创建也是允许的(注意取数据的时候要做类型转换)
List<String>[] ls = new ArrayList[10];
List<String>[] ls = new (List<String>[])ArrayList[10];
List<?>[] ls = new ArrayList<?>[10];
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
(3)可以使用steam库的toArray()加上Lambda表达式来创建对象数组
Person[]people = stream.toArray(Person::new):
在静态上下文中不允许类的参数是泛型类型(静态变量不可是泛型,静态方法要注明)
泛型类不能扩展异常(异常类不能是泛型的,因为运行时catch中要匹配类型)
(所有修饰符和关键字保留字整理:修饰符-> public protected private static abstract final 关键字 保留字-> )
(可访问修饰符决定了父类方法是否可以在子类中 重载 调用!一定注意)
(所有抽象类和接口中的抽象方法在子类中的实现都相当于方法重写,要加上@override 表明是重写(好习惯))
8 枚举类
有时候, 变量的取值只在一个有限的集合内。为了防止取值超出给定范围,我们可以自定义枚举类型
enum Size {SMALL, MEDIUM, LARGE, EXTR.ALARCE };//实际上定义了这个类以及4个对象
Size s = Size.MEDIUM;//实际上是在赋对象的引用
Size 类型的变量只能存储这个类型声明中给定的某个枚举值, 或者 null 值, null 表示这 个变量没有设置任何值。
实际上, 这个声明定义的类型是一个类, 它刚好有 4 个实例, 在此尽量不要构造新对象。 因此, 在比较两个枚举类型的值时, 永远不需要调用 equals, 而直接使用“ = = ” 就可以了。
如果需要的话, 可以在枚举类型中添加一些构造器、 方法和域。 当然,构造器只是在构造枚举常量的时候被调用(不是新建而是调用)。
public enum demo{
DEMO_SUCCESS(1,"成功"),DEMO_FAIL(0,"失败");//枚举常量
//对应的成员变量的属性值
private Integer code;
private String desc;
//公有的getter
public Integer getCode(){return code}
public String getDedc(){return desc}
//全参的构造方法(当调用某个枚举常量时,会将里面的值对应传递给构造方法为成员变量赋值)
Demo(Integer code,String desc){
this.code = code;
this.desc = desc;
}
}
所有的枚举类型都是 Enum 类的子类。它们继承了这个类的许多方法。其中最有用的一 个是 toString, 这个方法能够返回枚举常量名。 例如, Size.SMALL.toString( ) 将返回字符串“ SMALL”。
toString 的逆方法是静态方法 valueOf ,Size s = Enum.valueOf(Size,class, "SMALL");
将 s 设置成 Size.SMALL。
每个枚举类型都有一个静态的 values 方法, 它将返回一个包含全部枚举值的数组。
Size[] values = Size.values();
实际上,如同 Class 类一样, 鉴于简化的考虑, Enum 类省略了一个类型参数。 例如, 应该将枚举类型 Size扩展为 Enum<Size>
9 Lambda表达式
(数学上,带参数变量的表达式就被称为 lambda 表达式)
lambda 表达式是一个可传递的代码块(自带变量), 可以在以后执行一次或多次。
在 Java 中传递一个代码段并不容易, 不能直接传递代码段 。Java 是一种面向对象语言, 所以必须构造一个对象, 这个对象的类(接口)需要有一个方法能容纳用Lambda表达式传递的代码。这个时候就要借助接口,通过传递Lambda表达式,创建一个含有这个代码段的接口实例来间接实现代码段传递。
lambda 表达式不能独立存在, 总是会转换为函数式接口的实例。
实际上Lambda表达式就是一段自带变量的代码段,当把它传到函数式接口中实际上它的变量就等价于接口中那个抽象方法的变量,它的方法体就是那个抽象方法的重写。当作为变量传入某个方法时,就默认作为需要的实现该接口的类的实例而被创造(该方法所需要的实例)。本质上就是创建了一个接口的类的实例,并且把Lambda表达式的代码给塞进去了作为实现(实现了代码段的传递)
(1)Lambda表达式语法规则
- lambda表达式就是一个代码块, 以及必须传人代码的变量规范。无需指定 lambda 表达式的返回类型。lambda 表达式的返回类型总是会由上下文推导得出
(String first, String second)
-> first.length() - second.length()
注意!如果一个 lambda 表达式只在某些分支返回一个值, 而在另外一些分支不返回值, 这是不合法的。 例如,(int x) -> { if (x >= 0) return 1; } 就不合法。
- 一种 lambda 表达式形式: 参数, 箭头(->) 以及一个表达式。
如果代码要完成的计算无法放在一个表达式中, 就可以像写方法一样, 把这些代码放在 {}中, 并包含显式的 return 语句。
(String first, String second) ->
{ if (first.lengthO < second.lengthO) return -1;
else if (first.lengthO > second.length()) return 1;
else return 0; }
- 即使 lambda 表达式没有参数, 仍然要提供空括号, 就像无参数方法一样:
() -> { for (int i= 100; i>= 0; i-- ) System.out.println(i); }
- 如果可以推导出一个 lambda 表达式的参数类型, 则可以忽略其类型。
Comparator<String> comp
= (first, second) // Same as (String first, String second)
-> first.length() - second.length();
在这里, 编译器可以推导出 first 和 second 必然是字符串, 因为这个 lambda 表达式将赋给一个字符串比较器。
- 如果方法只有一个参数,而且这个参数的类型可以推导得出,那么甚至还可以省略小括号:
ActionListener listener = event ->
System.out.println("The time is " + new Date());
// Instead of (event) -> . . . or (ActionEvent event) -> . . .
(2)函数式接口
对于只有一个抽象方法的接口, 需要这种接口的对象时, 就可以提供一个 lambda 表达式。 这种接口称为函数式接口 (functional interface)。
注意!接口不一定有抽象方法。
一方面,接口完全有可能重新声明 Object 类的方法, 如 toString 或 clone, 这些声明有可能会让方法不再是抽象的。(Java API 中的一些接口会重新声明 Object 方 法 来附加javadoc注释 ComparatorAPI 就是这样一个例子)
另一方面,在 JavaSE 8 中,接口可以声明非抽象方法(静态方法和默认方法)。
将Lambda表达式转换为函数式接口的实现对象使用(Lambda表达式的常规用法)
对于Arrays.sort 方法,它的第二个参数需要一个 Comparator 实例, Comparator 就是只有一个方法的接口, 所以可以提供一个 lambda 表达式(相当于实现了这个函数式接口的抽象方法):
Arrays.sort (words ,
(first, second) -> first.length() - second.length());
在底层,Arrays.sort 方法会接收实现了Comparator<String>
的某个类的对象。在这个对象上调用 compare方法会执行这个lambda 表达式。
这些对象和类的管理完全取决于具体实现,与使用传统的内联类相比,这样可能要高效得多。最好把 lambda表达式看作是一个函数,而不是一个对象,另外要接受 lambda 表达式可以传递到函数式接口。
实际上,在 Java 中,对 lambda 表达式所能做的也只是能转换为函数式接口。在其他支持函数字面量的程序设计语言中,可以声明函数类型(如(String,String)-> int)、声明这些类型的变量, 还可以使用变量保存函数表达式。
甚至不能把 lambda 表达式赋给类型为Object 的变量,Object 不是一个函数式接口。
在Java中想要用 lambda 表达式做某些处理,还是要谨记表达式的用途,为它建立一个特定的函数式接口。
Java API 在 java.util.fimction 包中定义了很多非常通用的函数式接口。
常用的函数式接口:
BiFunction<T, U, R>
描述了参数类型为 T 和 U 而且返回类型为 R 的函数。可以把我们的字符 串比较 lambda 表达式保存在这个类型的变量中:
BiFunction<String, String, Integer> comp
= (first, second) -> first.lengthO - second.length();
- java.util.function 包中有一个尤其有用的接口
Predicate
:
public interface Predicate<T>{
boolean test(T t);
//Addition default and static method
}
ArrayList 类有一个 removelf 方法, 它的参数就是一个 Predicate。这个接口专门用来传递 lambda 表达式(实际上就是将Lambda作为test的实现,是具体的比较逻辑)。 例如, 下面的语句将从一个数组列表删除所有 null 值:
list.removelf(e -> e == null);
(3)方法引用
使用方法引用和lambda表达式等价
类似于 lambda 表达式, 方法引用不能独立存在, 总是会转换为函数式接口的实例。
表达式System.out::println
是一个方法引用(method reference), 它等价于 lambda 表达式x 一> System.out.println(x)
用 :: 操作符分隔方法名与对象或类名。 主要有 3 种情况:
- object::instanceMethod
- Class ::static Method
- Class ::instanceMethod
在前 2 种情况中, 方法引用等价于提供方法参数的 lambda 表达式。 前面已经提到, System.out::println 等价于 x -> System.out.println(x)。 类似地, Math::pow 等价于(x,y) -> Math.pow(x, y)。
对于第 3 种情况, 第 1 个参数会成为方法的目标。例如,String::compareToIgnoreCase 等 同于 (x, y) -> x.compareToIgnoreCase(y)
如果有多个同名的重栽方法, 编译器就会尝试从上下文中找出你指的那一个方法。 例如, Math.max 方法有两个版本, 一个用于整数, 另一个用于 double 值。 选择哪一个版 本取决于 Math::max 转换为哪个函数式接口的方法参数。
可以在方法引用中使用 this 参数。 例如, this::equals 等同于 x -> this.equals(x)。 使用 super 也是合法的,比如super::instanceMethod
(4)构造器引用
构造器引用与方法引用很类似, 只不过方法名为 new。例如,Person::new 是 Person 构造器的一个引用。选择哪一个构造器取决于上下文。
构造器引用可以很好地解决无法创建泛型数组的问题
Stream接口有一个 toArray 方法可以返回 Object 数组,可以把 Person[]::new 传入toArray 方法,toArray 方法调用这个构造器来得到一个正确类型的数组。 然后填充这个数组并返回。
Person[] people = stream.toArray(Person::new):
(5)变量作用域
通常,你可能希望能够在 lambda 表达式中访问外围方法或类中的变量,Lambda表达式可以捕获它所使用的外围方法或类中的变量。
lambda 表达式有 3个部分:
(1)一个代码块
(2)参数
(3)自由变量的值,这是指非参数而且不在代码中定义的变量
注意!表示 lambda 表达式的数据结构必须存储自由变量的值,我们说它被 lambda 表达式捕获(captured)(比如可以把一个 lambda 表达式转换为包含一个方法的对象, 这样自由变量的值就会复制到这个对象的实例变量中。)
关于代码块以及自由变量值有一个术语: 闭包(closure)。在 Java 中,lambda表达式就是闭包。
lambda 表达式可以捕获外围作用域中变量的值。 在 Java 中,要确保所捕获的值是明确定义的,这里有一个重要的限制。在 lambda 表达式中, 只能引用值不会改变的变量(比如String),否则无论是在表达中改变这个变量还是变量在表达式外被改变,都会产生严重问题(比如并发执行)
lambda 表达式中捕获的变量必须实际上是最终变量 ( effectivelyfinal )。 最终变量是指,这个变量初始化之后就不会再为它赋新值。
public static void countDown(int start, int delay)
{
ActionListener listener = event ->
{
start--; // Error: Can't mutate captured variable
System.out.println(start) ;
};
new Timer(delay, listener),start();
}
lambda 表达式的方法体与嵌套块有相同的作用域。 这里同样适用命名冲突和遮蔽的有关规则。 在 lambda 表达式中声明与一个局部变量同名的参数或局部变量是不合法的,因为在方法中不能有两个同名的局部变量, 因此,lambda 表达式中同样也不能有同名的局部变量。
在一个lambda 表达式中使用 this 关键字时, 是指创建这个 ambda 表达式的方法的 this 参数。
(6)处理Lambda表达式
使用 lambda 表达式的重点是延迟执行(deferred execution)。 毕竟, 如果想耍立即执行代 码, 完全可以直接执行, 而无需把它包装在一个lambda 表达式中。 希望延迟执行的场景有很多,例如:
(1)在一个单独的线程中运行代码
(2)多次运行代码
(3)在算法的适当位置运行代码(例如, 排序中的比较操作)
(4)发生某种情况时执行代码(如数据到达、点击按钮 等)
(5)只在必要时才运行代码
常用函数式接口:核心技术PDF P256
最好使用提供的标准的函数式接口,如果设计你自己的接口, 其中只有一个抽象方法, 可以用 @FunctionalInterface
注解来标记这个接口。 这样做有两个优点。 如果你无意中增加了另一个非抽象方法, 编译器会产生一个错误消息。 另外 javadoc 页里会指出你的接口是一个函数式接口。
Comparator 接口包含很多方便的静态方法来创建比较器。 这些方法可以用于 lambda 表达式或方法引用。
静态 comparing 方法取一个“ 键提取器” 函数, 它将类型 T 映射为一个可比较的类型 ( 如 String )。 对要比较的对象应用这个函数, 然后对返回的键完成比较。 例如, 假设有一个 Person 对象数组, 可以如下按名字对这些对象排序:
Arrays.sort(people, Comparator.comparing(Person::getName));
Arrays.sort( people , Comparator.comparing(Person::getlastName) .thenConiparing(Person::getFirstName));
···//后面的实在是看不明白了
10 内部类
内部类(inner class) 是定义在另一个类中的类。
使用内部类的原因:
(1)内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
(2)内部类可以对同一个包中的其他类隐藏起来。
(3)当想要定义一个回调函数且不想编写大量代码时,使用匿名(anonymous) 内部类比较便捷。
嵌套是一种类之间的关系, 而不是对象之间的关系。一个 LinkedList 对象并不包含 Iterator 类型或 Link 类型的子对象。
嵌套类有两个好处: 命名控制和访问控制。
由于Iterator 嵌套在 LinkedList 类的内部, 所以在外部被命名为 LinkedList::Iterator,这样就不会与其他名为 Iterator 的类 发生冲突。在 Java 中这个并不重要, 因为 Java 包已经提供了相同的命名控制。
如果将Link内部类声明为private。即使将 Link 的数据域设计为公有的, 它仍然是安全的。 这些数据域只能被 LinkedList 类 (具有访问这些数据域的合理需要)中的方法访问, 而不会暴露给其他的代码。 在 Java 中, 只有内部类能够实现这样的控制。
(1)内部类基础
Java 内部类还有另外一个功能, 这使得它比 C++ 的嵌套类更加丰富,用途更加广泛。
内部类的对象有一个隐式引用,它引用了实例化该内部对象的外围类对象。通过这个指针, 可以访问外围类对象的全部状态(?)。
在 Java 中,static 内部类没有这种附加指针,这样的内部类与 C++ 中的嵌套类很相似。
从传统意义 上讲, 一个方法可以引用调用这个方法的对象数据域。 内部类既可以访问自身的数据域, 也 可以访问创建它的外围类对象的数据域.
内部类的对象总有一个隐式引用, 它指向了创建它的外部类对象,这个引用在内部类的定义中是不可见的。
外围类的引用在构造器中设置。 编译器修改了所有的内部类的构造器, 添加一个外围类 引用的参数。(当外围类创建一个内部类对象时,就会隐式的将this传给它(编译器自动完成))
只有内部类可以是私有类, 而常规类只可以具有包可见性, 或公有可见性。(可以将内部类声明为private,同样的可以声明为public(没有唯一性限制))
表达式OuterClass.this
表示在内部类中外围类引用。outerObject.new InnerClass(constructionparameters)
表示内部类的构造器(创建内部类对象时用)
在外围类的作用域之外, 可以这样引用内部类:OuterClass.InnerClass
内部类中声明的所有静态域都必须是 final。 原因很简单。 我们希望一个静态域只 有一个实例, 不过对于每个外部对象, 会分别有一个单独的内部类实例。 如果这个域不 是final, 它可能就不是唯一的。
内部类不能有 static 方法。Java 语言规范对这个限制没有做任何解释。也可以允许有静态方法, 但只能访问外围类的静态域和方法。
内部类是一种编译器现象, 与虚拟机无关。 编译器将会把内部类翻译成用 $ ( 美元符号)分隔外部类名与内部类名的常规类文件, 而虚拟机则对此一无所知。例如, 在TalkingClock 类内部的 TimePrinter 类将被翻译成类文件 TalkingClock$Time Printer.class
由于内部类拥有访问特权,可以访问外围类的私有数据, 所以与常规类比较起来功能更加强大。
(这样做不是存在安全风险吗? 这种担心是很有道理的。 任何人都可以通过调用 accessSO 方法很容易地读取到私有域 beep。 当然, access$0 不是 Java 的合法方法名。 但熟悉类文件结构的黑客可以使用十六进制编辑器轻松地创建一个用虚拟机指令调用那个方法的类文件。 由于隐秘地访问方法需要拥有包可见性, 所以攻击代码需要与被攻击类放在同一个包中。)
合成构造器和方法是复杂令人费解的(所以就先不看了)
(2)局部内部类(在方法中定义的内部类)
只有一个方法使用该内部类时,可以将这个内部类在方法中声明。
局部类不能用 public 或 private 访问说明符进行声明。它的作用域被限定在声明这个局部 类的块中。
局部类有一个优势, 即对外部世界可以完全地隐藏起来。 即使 TalkingClock 类中的其他 代码也不能访问它。除 start 方法之外, 没有任何方法知道 TimePrinter 类的存在。
与其他内部类相比较, 局部类还有一个优点。 它们不仅能够访问包含它们的外部类, 还 可以访问局部变量。 不过, 那些局部变量必须事实上为 final才能访问。 这说明,它们一旦赋值就绝不会改变。
(3)匿名内部类
将局部内部类的使用再深人一步。假如只创建这个类的一个对象, 就不必命名了。这种
类被称为匿名内部类(anonymous inner class)。通常的语法格式为:
new SuperType(construction parameters) {
inner class methods and data
}
其中,SuperType 可以是 ActionListener 这样的接口, 于是内部类就要实现这个接口。 SuperType 也可以是一个类, 于是内部类就要扩展它。
由于构造器的名字必须与类名相同, 而匿名类没有类名, 所以, 匿名类不能有构造器。 取而代之的是, 将构造器参数传递给超类(superclass) 构造器。尤其是在内部类实现接口的时候,不能有任何构造参数(因为接口没有父类)。
new InterfaceType(){
methods and data
}
(4)静态内部类
当使用内部类只是为了把一个类隐藏在另外一个类的内部, 并不需要内部类引用外围类对象。为此,可以将内部类声明为static, 以便取消产生的引用。
在内部类不需要访问外围类对象的时候, 应该使用静态内部类。 有些程序员用嵌套类(nestedclass) 表示静态内部类。
与常规内部类不同, 静态内部类可以有静态域和方法。
声明在接口中的内部类自动成为 static 和 public 类。
11 代理
利用代理可以在运行时创建一个实现了一组给定接口的新类 : 这种功能只有在编译时无法确定需要实现哪个接口才有必要使用。
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!