Java-初级&高级特性梳理
OOP
OOP的三大特性
面向对象的程序设计方法具有三个基本特征:封装、继承、多态。
封装
封装:将对象的实现细节隐藏起来,然后通过一些公用方法来暴露该对象的功能。
继承
继承是面向对象实现软件复用的重要手段,当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法;
多态
多态分为编译时多态和运行时多态。
编译时多态表现为重载,重载是同一类下的不同方法,通过同一函数名不同的参数列表表现出对象的多样性。
运行时多态表现为继承、重写、向上转型。子类继承父类之后,重写父类方法后通过向上转型,调用重写方法时表现出不同的子类的行为特征。
Q:为什么用接口定义变量,而非具体的实现类?(比如
List<Integer> list = new ArrayList<>();
List为接口)A:用接口定义变量只需关注接口特性,而无需关注具体实现,这体现面向接口编程思想;同时,如果想换一种实现类,只需更改这一行代码就可以实现。
向上转型 & 向下转型
向上转型
用子类实例化父类对象,属于自动转换。
向上转型的意义在于不同子类可以重写父类方法,可以统一用定义的父类对象调用得到子类方法的结果,减少了代码量,体现了抽象编程的思想。
子类重写父类方法的条件:1、父类方法对子类可见(不能是private);2、子类和父类的方法必须是实例方法(不能是static);3、父类和子类的方法需要有相同的函数名、参数列表、返回类型(子类可以是父类的子类型);4、子类访问权限不能小于父类;5、子类不能抛出比父类范围更大的异常;参考地址
向下转型
用父类实例化子类对象,强制转换。用 instance of
进行判断后强转。
重载 & 重写
重载
重载是同一个类中的不同方法,方法名相同而参数列表不同,与返回类型和作用域无关。
重写
重写发生在子类父类中,子类对父类的方法重写时,函数名和参数列表要相同,返回类型要小于等于父类,作用域要大于等于父类,抛出异常的范围要小于等于父类。
Q:构造方法能重写吗?
构造方法不能重写。
首先,子类重写父类方法时,方法名要保持一致(子类的构造函数名要和父类的构造函数名一致);其次,构造方法要和类名保持一致(子类构造方法要和子类名保持一致,父类构造方法要和父类名一致);这就推出子类名要和父类名一致,但是二者是不同的,所以构造方法不能重写。
Object类的方法
clone()
: 浅拷贝,需要实现Cloneable
接口。toString()
hashCode()
: 获取对象的哈希值,一般用于比较两个对象是否相等的初判断条件equals()
: 判断两个对象是否相等,一般情况下equals()
和==
是不一样的,只有在Object中两者才是一样的getClass()
: 获取运行时类型wait()
: 是当前线程等待该对象的锁notify()
: 唤醒在该对象等待的某线程notifyAll()
: 唤醒在该对象等待的所有线程
深拷贝和浅拷贝
浅拷贝:只是对被拷贝对象的变量进行拷贝,如果被拷贝对象的变量指向上一层对象,那拷贝对象的变量也只想那个上层对象,此时修改上层对象,拷贝对象的变量和被拷贝的对象的变量同时修改。
深拷贝:对被拷贝的变量所指向的上层对象也进行拷贝,此时如果修改被拷贝对象上层对象属性,拷贝后的对象不会受到影响。
深拷贝的实现:
- 通过重写Cloneable的clone方法,对拷贝对象引用的所有对象也进行拷贝
- 通过序列化的方式,写到输出流,再读取出来,自然得到的是一个新对象
关键字
super
super关键字是一个引用变量,用于引用直接父类对象。
super.fatherAttribute
:访问父类属性super.fatherMethod()
:访问父类方法super()
:访问父类构造方法
static
static是为了满足两个需求产生的:1、为特定域分配单一存储空间;2、不需要通过反复创建对象调用方法。
java类有五个组成部分:属性、成员方法、构造方法、初始化块、内部类。static可以作用在除构造方法的其他成分上,被static修饰的成分是属于类的,而非某个实例。
对static使用的说明:
- static类可以被继承,但被static修饰的方法不能在子类被重写(方法重写是运行绑定的,而static方法是编译时绑定的);
- static方法不能引用非static资源,但非static方法可以引用static资源;
- static修饰内部类也可以被外部类的实例访问,因为static作用的成分属于公共资源;
final
final修饰在不同成分上表现出不同的特征。
- 修饰类:不可以被继承;
- 修饰方法:不可以被重写;
- 修饰变量:获得初始值,就不可以被修改。
被final修饰的常见的类:八大封装类,void,字符串类(String, Stringbuffer, StringBuilder),Array等。
抽象类 & 接口
抽象类
Q:java为什么要有抽象类?
提供实现不同子类公用的方法。
接口
抽象类和接口的区别
泛型
泛型定义
泛型是jdk5引入的新特性,本质为了参数化类型,提供了类型安全检测机制,实现了一次编写模板,多次复用。
List里可以装不同类型的元素,在编写底层List底层代码时,如果不使用泛型,对于每个方法都要根据不同如参类型进行重载,代码重用性极差;
同时,底层实现是Object[],传不同的数据类型,要进行强制转换,这会导致容易产生类型转换错误等。
1 |
|
❗️❗️泛型getClass()
方法获取Class类型。
编写泛型
编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用T
, E
or K
,V
等被广泛认可的类型占位符。泛型方法的例子请参阅Java集合类框架。在函数返回类型,和入参类型需要使用泛型的地方都需要用占位符占位。
1 |
|
泛型的Java实现(擦拭法)
泛型只在编译阶段有效,在编译过程中会进行类型擦除,并在对象进入和离开方法的时候进行类型转换和类型检测,而泛型信息不会进入到运行时阶段。
Java语言的泛型实现方式是擦拭法(Type Erasure)。
擦拭法是指,JVM完全屏蔽泛型,所有的类型转换工作都是编译器做的。
1 |
|
擦拭法决定了泛型
- 不能是基本类型,例如:int;
- 不能获取带泛型类型的Class,例如:Pair
.class; - 不能判断带泛型类型的类型,例如:x instanceof Pair
; - 不能实例化T类型,例如:new T()。
extends & super通配符
向上转型
❗️❗️向上转型是谁转?是外层的类型转,而不是<>里的类型转!
对于ArrayList<String>
来说,向上转型的结果就是List<String>
。
但是对于ArrayList<Integer>
,ArrayList<Number>
并不是其向上转型的结果!
如果想对<>
里面的内容进行向上或向下类型转换,要怎么做? 向上转->extends,向下转->super
extends 上界通配符
extends Number>通配符的一个重要限制:方法参数签名setFirst(? extends Number)无法传递任何Number的子类型给setFirst(? extends Number) 使用 extends Number>通配符表示: 1. 使用extends通配符表示可以读,不能写; 2. 使用类似< T extends Number>定义泛型类时表示:泛型类型限定为Number以及Number的子类。1 |
|
1 |
|
通配符既没有extends,也没有super,因此:
- 不允许调用set(T)方法并传入引用(null除外);
- 不允许调用T get()方法并获取T引用(只能获取Object引用)。
无限定通配符<?>很少使用,可以用
泛型的使用
Q:在哪些地方可以使用泛型?泛型有哪些使用方式?
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。
泛型类
泛型类,就是用泛型来定义类。通过泛型可以完成对一组类的操作对外开放相同的接口。
最典型的就是各种容器类,如:List、Set、Map。
1 |
|
泛型接口
泛型接口,就是用泛型来定义接口。泛型接口常被用在各种类的生成器中。
1 |
|
泛型方法
基本用法
1 |
|
泛型类中的泛型方法
1 |
|
可变参数的泛型方法
1 |
|
静态泛型方法
1 |
|
泛型数组
不能创建一个确切的泛型类型的数组!原因在于JVM泛型擦除在运行时装入一个元素不会异常,但是在取值的时候可能会出现不一致的情况。
1 |
|
其他问题
- Array中可以用泛型吗?
Array是不可以使用泛型的。这是因为List可以提供编译期的类型安全保证,而Array却不能。
参考资料
注解
反射
核心知识点
反射是Java在运行时,可以动态地获取对象的所有属性和方法的一种机制。
反射通过Class类获取类的class对象;通过Constructor类反映类的构造方法;通过Field类反映类的属性;通过Method类反映类的方法。
Class类是一个类(java.lang),其实例表示java运行时的类或接口。
反射的使用
Class类对象的获取
在类加载的时候,jvm会创建一个class对象。
获取class对象的方式的主要有三种:
- 根据类名:类名.class
- 根据对象:对象.getClass()
- 根据全限定类名:Class.forName(全限定类名)
1 |
|
Constructor类
获取Constructor
对象是通过Class
类中的方法获取的。
方法返回值 | 方法名称 | 方法说明 |
---|---|---|
static Class<?> | forName(String className) | 返回与带有给定字符串名的类或接口相关联的 Class 对象。 |
Constructor | getConstructor(Class<?>… parameterTypes) | 返回指定参数类型、具有public访问权限的构造函数对象 |
Constructor<?>[] | getConstructors() | 返回所有具有public访问权限的构造函数的Constructor对象数组 |
Constructor | getDeclaredConstructor(Class<?>… parameterTypes) | 返回指定参数类型、所有声明的(包括private)构造函数对象 |
Constructor<?>[] | getDeclaredConstructor() | 返回所有声明的(包括private)构造函数对象 |
T | newInstance() | 调用无参构造器创建此 Class 对象所表示的类的一个新实例。 |
Constructor
类常用的API:
方法返回值 | 方法名称 | 方法说明 |
---|---|---|
Class | getDeclaringClass() | 返回 Class 对象,该对象表示声明由此 Constructor 对象表示的构造方法的类,其实就是返回真实类型(不包含参数) |
Type[] | getGenericParameterTypes() | 按照声明顺序返回一组 Type 对象,返回的就是 Constructor对象构造函数的形参类型。 |
String | getName() | 以字符串形式返回此构造方法的名称。 |
Class<?>[] | getParameterTypes() | 按照声明顺序返回一组 Class 对象,即返回Constructor 对象所表示构造方法的形参类型 |
T | newInstance(Object… initargs) | 使用此 Constructor对象表示的构造函数来创建新实例 |
String | toGenericString() | 返回描述此 Constructor 的字符串,其中包括类型参数。 |
Field类
获取Field
对象是通过Class
类中的方法获取的。
方法返回值 | 方法名称 | 方法说明 |
---|---|---|
Field | getDeclaredField(String name) | 获取指定name名称的(包含private修饰的)字段,不包括继承的字段 |
Field[] | getDeclaredField() | 获取Class对象所表示的类或接口的所有(包含private修饰的)字段,不包括继承的字段 |
Field | getField(String name) | 获取指定name名称、具有public修饰的字段,包含继承字段 |
Field[] | getField() | 获取修饰符为public的字段,包含继承字段 |
Field
类常用的API:
方法返回值 | 方法名称 | 方法说明 |
---|---|---|
void | set(Object obj, Object value) | 将指定对象变量上此 Field 对象表示的字段设置为指定的新值。 |
Object | get(Object obj) | 返回指定对象上此 Field 表示的字段的值 |
Class<?> | getType() | 返回一个 Class 对象,它标识了此Field 对象所表示字段的声明类型。 |
boolean | isEnumConstant() | 如果此字段表示枚举类型的元素则返回 true;否则返回 false |
String | toGenericString() | 返回一个描述此 Field(包括其一般类型)的字符串 |
String | getName() | 返回此 Field 对象表示的字段的名称 |
Class<?> | getDeclaringClass() | 返回表示类或接口的 Class 对象,该类或接口声明由此 Field 对象表示的字段 |
void | setAccessible(boolean flag) | 将此对象的 accessible 标志设置为指示的布尔值,即设置其可访问性 |
Method类
获取Method
对象是通过Class
类中的方法获取的。
方法名称 | 方法说明 | |
---|---|---|
Method | getDeclaredMethod(String name, Class<?>… parameterTypes) | 返回一个指定参数的Method对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法。 |
Method[] | getDeclaredMethod() | 返回 Method 对象的一个数组,这些对象反映此 Class 对象表示的类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。 |
Method | getMethod(String name, Class<?>… parameterTypes) | 返回一个 Method 对象,它反映此 Class 对象所表示的类或接口的指定公共成员方法。 |
Method[] | getMethods() | 返回一个包含某些 Method 对象的数组,这些对象反映此 Class 对象所表示的类或接口(包括那些由该类或接口声明的以及从超类和超接口继承的那些的类或接口)的公共 member 方法。 |
Method
类常用的API:
方法返回值 | 方法名称 | 方法说明 |
---|---|---|
Object | invoke(Object obj, Object… args) | 对带有指定参数的指定对象调用由此 Method 对象表示的底层方法。 |
Class<?> | getReturnType() | 返回一个 Class 对象,该对象描述了此 Method 对象所表示的方法的正式返回类型,即方法的返回类型 |
Type | getGenericReturnType() | 返回表示由此 Method 对象所表示方法的正式返回类型的 Type 对象,也是方法的返回类型。 |
Class<?>[] | getParameterTypes() | 按照声明顺序返回 Class 对象的数组,这些对象描述了此 Method 对象所表示的方法的形参类型。即返回方法的参数类型组成的数组 |
Type[] | getGenericParameterTypes() | 按照声明顺序返回 Type 对象的数组,这些对象描述了此 Method 对象所表示的方法的形参类型的,也是返回方法的参数类型 |
String | getName() | 以 String 形式返回此 Method 对象表示的方法名称,即返回方法的名称 |
boolean | isVarArgs() | 判断方法是否带可变参数,如果将此方法声明为带有可变数量的参数,则返回 true;否则,返回 false。 |
String | toGenericString() | 返回描述此 Method 的字符串,包括类型参数。 |
异常
异常的层次结构
结构图解析
Throwable
:异常和错误的超类,只有Throwable的实例才能被throw或者catch。Error
: 程序中无法处理的错误,表示程序运行时JVM出现问题。比如OOM、StackOverflow;Exception
: 程序本身可以捕获并且可以处理的异常。RuntimeException
: 由程序逻辑错误引起的,编译器不会检查,也不会捕获处理。(NullPointerExceptio,IndexOutOfBoundsException)非运行时异常
:必须进行处理的异常,如果不处理,程序就不能编译通过。(IOException, SQLException)
checkedExceptions
:除了RuntimeException
外的Exception
类及其子类;这种是编译器可以检测的,需要try…catch…或者throws,否则会报错。uncheckedExceptions
: 编译器不要求强制处理的异常。
Q:什么时候要往上层抛异常?
一般如果程序中出现checkedException
,编译器会提示进行异常处理。而抛异常一般指向上一层方法抛异常(因为本层如果可以处理就try…catch…捕获了)。而向上抛异常一般可能有以下两点考虑:
- 传递一个具体的危险信号,让调用方知道;(catch后抛出一个新的类型的异常)
- 本层方法没有能力处理,返回给调用方处理;(当前层方法被不只一个地方调用,业务逻辑并不一致,不能做同一处理,需要向上层抛出;)
Q:怎么抛异常?
在方法里可以throw一个异常,或者在方法签名throws若干异常。
IO
序列化
序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。
一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable
接口。
把一个Java对象变为byte[]数组,需要使用ObjectOutputStream
,从一个字节流读取Java对象用ObjectInputStream
。
IO模型
todo
IO多路复用
todo
容器
容器间的继承关系
HashMap
put 过程分析
- 第一次put的时候,会触发resize操作,第一次resize默认的数组大小是16;
- 通过hash映射,得到当前key要put到的数组位置(数组下标是 arrlen & hash(key) 得到的);
- 如果当前数组里的位置为空,新建一个KV的node放到数组里;(node 包含的属性有key, value, next, hash)
- 如果数组里已经有数据,取出当前结点判断key与要put的key是否一致,如果一致说明要覆盖旧值,如果不一致,说明哈希冲突;
- 如果哈希冲突,需要对冲突的node进行类型判断,如果是红黑树,需要通过红黑树的插值方法完成put操作;否则是链表,通过拉链法完成插值;
- 拉链法插值的过程为:要遍历链表,如果找到node的key与当前put的node的key一致,进行覆盖;否则将node加到链表尾部;链表长度超过8要转成红黑树的操作;
- 哈希冲突判断完,接下来剩下两个判断操作;一个是在key相等时进行覆盖;另一个是如果因为put达到阈值要进行扩容。
1 |
|
扩容分析
- 在扩容代码里,主要有两个内容;1、针对capacity 和threshold 进行更新;2、进行数据迁移;
- 针对容量和阈值进行更新的时候,有两种情况;1、第一次put 数组初始化的hashmap;2、对有值的数组进行扩容;
- 对有值的数组进行扩容为一般情况,采取的扩容策略为将cap和thr分别扩大一倍(边界case:对>=2^30的cap直接扩容到Integer.MAX_VALUE);
- 如果是初始化的数组,就直接返回了;如果是已经有值的数组,需要进行数据迁移;
- 在数据迁移的时候,要将链表拆成两个链接。具体操作有两步;1、将数据迁移,用拉链法解决hash冲突;2、将两条链表分别挂在数组对应的下标上;
- 数据迁移时,遍历数组每一个元素:1、如果只有一个值,按key进行hash映射到新的地址就可以了;2、如果是红黑树,不展开;3、如果是链表,拿key的hash与oldcap做&,将最高位为0or1作为两个链表其中的一个的区分,在新得到的位置用拉链法解决哈希冲突(如果需要的话);
- 将链表挂载数组时,将低位链表挂在原位置上,将高位链表挂在原位置高一个oldcap的位置上。
1 |
|
get 过程分析
- 根据key做hash与当前数组容量做& 确定下标位置;
- 如果当前位置的元素的key和get的key一致,直接返回;
- 否则看当前元素类型,如果是红黑树,调用红黑树的查找方法查找;如果是链表,遍历拉链进行查找返回;
1 |
|
TreeMap
todo
LinkedHashMap
todo
Java8 新特性
Lambda表达式
简介
函数式编程
Java8之前用的都是静态方法或实例方法,Java8新增了函数方法,即把函数作为基本运算单元,函数可以作为变量,可以接收函数,还可以返回函数。
lambda表达式是函数式编程,lambda表达式的类型必须是函数式接口,被@FunctionalInterface
注解修饰。参考地址
函数式编程是将函数(一段操作)作为一个基本单位进行传递。以前的Java中参数只能是具体的变量,函数式编程打破这一规范,可以将整个方法作为一个参数传递。
lambda表达式语法
1 |
|
FunctionalInterface(函数式接口)
有且仅有一个抽象方法的接口。比如:Comparator
、Callable
、Runnable
等。
虽然Comparator接口有很多方法,但只有一个抽象方法int compare(T o1, T o2),其他的方法都是default方法或static方法。另外注意到boolean equals(Object obj)是Object定义的方法,不算在接口方法内。因此,Comparator也是一个FunctionalInterface。
方法引用
方法引用:使用操作符 “ ::” 将方法名和对象或类的名字分隔开来。主要有:对象::实例方法 类::静态方法 类::构造方法
1 |
|
特性
- lambda表达式内可以使用方法引用,仅当该方法不修改lambda表达式提供的参数;
- lambda内部可以使用静态、非静态和局部变量,这称为lambda内的变量捕获;
- lambda表达式只能引用 final 或 final 局部变量,这就是说不能在lambda内部修改定义在域外的变量;
1 |
|
Steam
Steam 的特点
java.io | java.util.stream | |
---|---|---|
存储 | 顺序读写的byte 或char |
顺序输出的任意Java对象实例 |
用途 | 序列化至文件或网络 | 内存计算/业务逻辑 |
java.util.List | java.util.stream | |
---|---|---|
元素 | 已分配并存储在内存 | 可能未分配,实时计算 |
用途 | 操作一组已存在的Java对象 | 惰性计算 |
惰性计算:一个
Stream
转换为另一个Stream
时,实际上只存储了转换规则,并没有任何计算发生。
1 |
|
Steam 的创建
Stream.of()
1 |
|
基于数组或Collection
1 |
|
基于Supplier
1 |
|
map
Map就是把一个Stream转换为另一个Stream。
1 |
|
reduce
reduce是一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果,即一个 Java对象。
1 |
|
filter
所谓filter()操作,就是对一个Stream的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Stream。
1 |
|
输出集合
1 |
|