背景
- Java程序常常会遇到一些蛋疼的bug,最后发现,都是在一些很基础的方面造成的。大量的时间花在调试代码找低级bug上是十分没有性价比的。所以,再系统梳理下Java,是十分必要的。
- 已经反复学习和使用Java多次了,但只要有段时间没用Java之后,每次使用前都想要重头再梳理一遍。本文章将更注重Java只是的系统性,而不是细节性。
- 本文是本人大脑的专属Cache,所以逻辑上可能只有我自己能够看懂,见谅。
一、目录
- Java继承
- Java反射
- Java接口
- Lambda表达式
- Java异常
二、Java继承
Java与C++继承
- 子类 extends 父类,而C++是:符号
- Java只有公有继承,C++有公有继承和私有继承
- Java只支持单继承,多继承通过接口实现
- 子类从父类继承所有的数据域和方法,但有一些不一定能够访问到。
- 此外,子类只能通过覆盖来修改,以及增加方法,但绝对无法删除父类的任何方法和数据域。
覆盖方法
- 覆盖:子类方法重写父类的方法,父类方法将会被覆盖,区别重载(本类方法之间)
- 覆盖时,子类和父类要严格一致(访问类型,返回值,方法名,参数列表)
- 可以通过@Override来对子类的覆盖方法进行标记,来保障子类该方法成功覆盖了一个父类的方法,而不是定义了一个毫无相关的方法(没有匹配父类方法的时候,会编译报错)
- 被覆盖的父类方法依然可以通过super方法调用。
- super并不是像this那样是一个对象引用,super只是一个指示编译器调用父类方法的特殊关键字,所以不能将super赋值给另一个变量。
- super()调用父类的构造器
多态与动态绑定
- 多态:父类对象变量可以引用子类对象,且能够通过父类对象正确调用该对象的方法。
- 动态绑定:在运行时,能够自动选择调用哪个方法。
- 用途:有一组不同类型的对象数据,可以直接通过他们父类类型的数组来统一组织
- 不能将父类引用赋值给子类变量,但是可以将子类引用赋值给父类对象,且不需要强制类型转换。
方法调用的过程
- 对象在调用方法的时候,除了方法显式的参数之外,还有一个隐式参数,那便是对象本身,隐式参数不属于函数签名
- 1.根据方法名,选出本类和父类中所有候选方法(父类中的方法需要是public的)
- 2.根据参数列表,进行重载解析,获得调用方法名字和参数类型,即函数签名(考虑子类覆盖父类)
- 3.如果该方法是private、static、final、或者构造器,那么编辑器就可以确定调用哪个方法了,这个称为静态绑定
- 4.可能存在多态的情况时,虚拟机会考虑到隐式参数对象的实际类型,选择调用对应类中的方法。依次实现运行时的动态绑定。
阻止继承 final类和方法
- final类,不允许被继承
- final方法,子类不允许覆盖这个方法,final类中的所有方法都是final的。
对象强制类型转换
- 只能在继承链上进行类型转换
- 在将父类转成子类之前,最好使用instanceof检查是否转换错误
if(child instanceof Manager){ // 不需要判null
manager = child;
}
- 在一些参数传递的时候可能会使用
抽象类
- 为了进一步提升父类的通用能力
- 抽象类:含有一个或者多个抽象方法的类,就是抽象类
- 抽象类和抽象方法需要使用abstract来修饰
- 抽象类中是可以存在具体数据和具体方法的,可以存在一个抽象方法
- 抽象类是可以不包含抽象方法的
- 抽象类不能被实例化,必须通过子类实现所有的抽象方法才可以。
- 虽然抽象类不能实例化,但可以通过抽象类变量引用子类对象(这是正常的父类子类特点)。
受保护的访问
- private:仅对本类可见
- public:对所有类可见,无限制
- protected:对本类和子类,以及本包类可见
- 如果想一个方法或者数据能够被子类访问,需要设置protected修饰符
- 默认情况:对本包类可见,对子类不可见
修饰符 | 本类 | 本包 | 子类 | 其他包 |
---|---|---|---|---|
public | yes | yes | yes | yes |
private | yes | no | no | no |
protected | yes | yes | yes | no |
default | yes | yes | no | no |
Object类
- 所有类的超类
- Java中只有基本类型不是对象,其他都是对象,都具有Object的方法和数据
- equals():
- 在没重写的情况下,与==的效果是一致的,判断的是地址而不是实际的内容。
- String,Integer等类已经对equals()进行重写了。
- 我们在实现自己类的时候,需要自己来实现equals。(在实现自己实现equals后,还需要实现hashcode)(很套路,很多IDE是支持一键自动生成的)
- Class getClass():获得该对象所属于的类
- class.getName获得类名
- hashcode(): 返回对象的散列值
- 规定:两个对象,如果equals为true,那么hashcode一定要保证相同,即相等的对象有相同的hashcode。
- 默认的的hashcode都是参考了存储地址的,所以两个对象,默认的hashcode一定不一样,即便他们值可能是一样的。
- 因此当自己实现equals的时候,对应的也要实现hashcode,来保证重要规则
- 两个相等的对象必须要返回相同的hashcode码,但是相等的hashcode不一定是相等的变量,(hashcode的计算方式)
- java对象的hashcode设计是为了配合基于散列的集合,添加元素的时候,通过hashcode和equals来快速判断对象是否已经存在(大大减少了equals次数,比纯equals循环要好多了)
- toString():将对象以字符串形式输出,常用于日志之类。Object中默认的为类名+Hash值
重写equals(Object obj)方法(了解即可,可以IDE自动生成的,hh)
- 1.引用的是同一个对象吗
- 2.obj为null吗?
- 3.两个是同一个类吗
- 4.obj强制转化成本类
- 5.一次判断各个数据域是否相等
- 记得还要检查下hashcode是否要也要重写哦
泛型数组 ArrayList
- 普通数组无法再运行时更改数组的大小,可以使用泛型数组库,ArrayList
ArrayList<MyClass> arrayList = new ArrayList();
- Java老版本中的Vector也可以实现动态数组,但没ArrayList有效。
- add(),向数组中添加一个元素,如果容量不够,会自动扩展
- ArrayList不支持数组的[]访问方式
- get(index),获取数组中index位置的元素
- set(index,item),替换数组中的某个元素
- size(),类似数组的length
- arrayList.ensureCapacity(100),预分配100大小的数组
- trimToSize(),当保证不再向数组中添加元素了,调用将释放多余分配的空间
- arrayList.toArray(array),为了方便数组的访问,可以通过ArrayList构造,然后转换成普通数组并处理
// 一种很好的实践
ArrayList<X> list = new ArrayList<>();
// add item to list
X[] a = new X[list.size()];
list.toArray(a);
对象包装器与自动装箱
- 基本类型都有对应的类,Integer, Double,Boolean,Character,Void等
- 他们的超类为Number
- 一旦包装器创建了,就不允许更改包装在其中的值了
- 自动装箱和自动拆箱:能在基础类型和对应的类之间自动转换,这是由Java编译器实现的,添加了装箱拆箱语句
- Java的==号检测的是两个变量指向的地址是否相同,所以不同于基础类型,装箱后要用equals方法来判断值是否相等(他们重写了equals方法)
- 字符串Int转换:
int x = Integer.paseInt("1")
或者Integer.valueOf("100")
参数可变的方法
- Java方法的参数数目可以是可变的
- 省略号
...
,可以出现在参数列表的最后,其实和数组的效果的类似的
printf(String fmt, Object...args);
反射
Java反射机制能够在程序运行时,对于任意一个类,都能够知道这个类的所有属性和方法。对于任意一个对象,在有权限的情况下,能够调用方法和属性。即,动态获取信息以及动态调用对象方法的机制。
- 反射,能够分析类能力,能够动态操纵Java代码
- 主要用户工具构造,在实际的应用中使用不多(也不建议在应用应用开发中过多使用)
- Class类,Java运行时系统为每个类维护一个Class类
- getClass(),Name.class
接口
- 抽象类是对类的抽象,而接口是对行为的抽象。
- 接口中所有方法都自动是public,不需要额外加public修饰符(但在实现接口的类中,方法前必须加public)
- 接口中不能含有实例域(接口没有实例),但可以含有常量和静态常量
- 对于方法,接口中不能提供实现(Java8后支持默认实现)
- 接口不是类,不能通过new被实例化成对象。
- 不存在接口实例化的对象,但是存在接口变量,且接口变量可以引用实现了它的类的对象。
- 接口支持继承另一个接口
public interface Comparable { // java.lang.Comparable
int compareTo(Object obj);
}
public Test implements Comparable {
public int compareTo(Object obj) { // 实现类时候,需要public
return Double.compare(salary,obj.salary); // 相减比较不适合浮点数
}
}
Java常用的内置接口
- Compalrable
- Cloneable
接口的意义在于统一服务的对外接口:Java是一种强类型语言,使用接口来解决类型的问题,编译器不需要再执行的时候进行类型检查(因为编译器认为只要这个类实现了这个接口,就一定能处理)
接口与继承(抽象类)
- Java不支持多继承,因为多继承会使语言更复杂
- Java通过接口来实现多继承,一个类可以实现多个接口
Java8之后,接口中可以存在静态方法并实现之了,目的是,这样就可以避免某些工具类必须要提供伴随类了。
默认方法:
- 在我们实现接口的时候,很多时候只需要实现部分方法,默认方法为接口中的方法提供一个默认的实现。
- 用default修饰符,然后就可以在接口中简单实现这些方法了。
Lambda表达式
- 将一个代码块像参数一样传递到另一个对象中(定时器,响应等),这个代码块会在未来的某个时刻会被调用。
- 对比通过传入对象来实现,Lambda表达式更优雅,使得Java支持函数式编程。
(参数列表) -> {代码块}
,Lambda没有返回类型- 函数式接口:只含有一个抽象方法的接口,可以把Lambda赋值给一个函数式接口变量
@FunctionalInterface // 可选,用于编译检查是否只有一个抽象方法,类似Override
public interface FunctionalInterface<T>{
void accept(T t);
}
- Lambda表达式可以转化为对应的接口形式
- Lambda表达式实际上是一个函数,传入之后会在某处会被执行,而不是一个对象。
- 所以不能将一个Lambda表达式赋值给一个Object变量,因为Object不是一个函数式接口
- 当我们想使用Lambda的时候,需要为其提供一个函数式接口
- 方法引用:
rrays.sort(strings,String::compareToIgnoreCase)
,其实也很好理解,因为Lambda是函数 - Comparator函数式接口
Lambda表达式的变量作用域
- Lambda中的变量
- 代码块中声明的,自己的
- 参数传入的
- 自由变量,如何保证Lambda执行的时候,这些外部的变量还存在,没有被销毁?
- 捕获,闭包:Lambda会存储自由变量的值,称为捕获。Lambda的代码块以及自由变量组成一个闭包
- Lambda中只能引用值不会改变的变量/常量
- Lambda的体和块有相同的作用域,所以块中不能定义与Lambda同一块中的已有变量。
内部类
成员内部类
- 定义在另一个类中的类。
- 并不是每个外部类的对象实例都有一个内部类实例,内部类实例对象是由外部类的方法来触发创建的。
- 特点:
- 内部类可以访问外部类的实例域,因为内部类具有外部类的引用。
- 访问的外部实例域必须是final,这个和Lambda类似。
- 内部类对象中有一个隐式引用,它指向创建它的外部类对象(在内部类的构造函数中记录)。
- 局部内部类对同一个包中的其他类不可见
- 创建内部类的前提是必须先创建外部类
- 内部类可以访问外部类的实例域,因为内部类具有外部类的引用。
局部内部类
- 定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。
- 和局部变量一样,不能有public、protected、private以及static修饰符的。
匿名内部类
- 只创建这个类的对象,但并没有为该类提供名字。
- 使用的最多,在编写事件监听的代码时使用匿名内部类不但方便,而且使代码更加容易维护。
- 一般只用在事件监听,接口回调等
ActionListener listener = new AcitonListener() {
public void actionPerformed(ActionEvent event){
// do something
}
}
- 类似的,在安卓SDK,Swing中用的很多,能够简化代码。但有了Lambda,还能够更简化。
静态内部类
- 多一个static关键字,不需要依附外部类而存在。
对比Lambda表达式和匿名内部类
- 匿名内部类仍然是类,编译会生成.class文件,Lambda通过invokedynamic指令插入到主类对应的位置执行
- 对于匿名类,关键词 this 解读为匿名类,而对于 Lambda 表达式,关键词 this 解读为写就 Lambda 的外部类。
- Java 编译器编译 Lambda 表达式并将他们转化为类里面的私有函数
Java异常
异常层次结构
- Throwable
- Error
- Java运行时系统内部错误,以及资源耗尽错误
- 应用程序无法抛出,由系统自动抛出,必然导致程序终止
- Exception
- IOExpection
- 与程序无关,而与IO等有关的错误
- RuntimeException
- 由程序本身错误导致的异常(数组越界,null指针,错误类型转换)
受查与非受查异常
- 非受查异常:Error以及RuntimeException,不需要显示声明,因为这些异常是可以努力避免的,处理它们比消除它们要好多了。
- 受查异常:IOException,编译器会检查程序是否为这类异常提供了异常处理器,需要在程序中显示声明
- 声明受查异常
- 一个方法不仅可以告诉编译器参数和返回值,还可以告诉编译器可能存在的异常
- 什么时候需要给方法声明受查异常
- 1.方法的内部调用了某个抛出受查异常的方法
- 2.方法内部通过throw抛出了一个受查异常
// 声明受查异常,是throws不是throw,因为可以是一个异常列表
public void readFileFunc(File file) throws FileNotFoundException { }
// 抛出一个异常
throw new Exception();
异常捕获
- try-catch语句
- 若try子句中没有异常则跳过catch子句,程序正常返回。
- try子句中发生错误,终止try子句执行,程序无返回值。
- 若catch子句能够捕获异常,则直接执行catch子句中的处理语句。
- 若catch子句无法捕获,则将该异常传递到上级调用方法来处理。
- 处理策略
- 对于知道如何处理的受检异常,则直接捕获处理
- 对于不知道如何处理的受检异常,则传递到调用方进行处理
- 传递一个异常,需要在方法后添加
throws
关键字,告知调用方,提供需要的对应的异常处理器
- 捕获多个异常
try {
...
} catch(FileNotFoundException e){
...
} catch(IOException e){
}
- 再此抛出异常链,
e.initCause(pre_e)
finally子句
- 无论是否抛出异常,都会执行
- finally子句中也可能抛出异常,最好将try-catch和try-finally解耦
- 当try和finally中都有return的时候,finally中的return先执行,且最终的返回值会被finally覆盖。即,如果finally中有return,try中的return无意义
try {
try{ // 内部try用于释放资源
...
} finally {
io.close();
}
} catch (IOExpection){
// error message
}
- 带资源的try,自动解决资源释放处理问题
try (Resource res = ...){
res...
}// 执行完毕之后,会自动调用res.close()
使用异常须知
- 异常的开销比较大,不要使用异常来实现正常的业务。只在异常情况下使用异常
- 不要过于细分异常
- 不要压制异常,便于分析程序错误
断言
- 断言机制允许在测试期间向代码中插入一些检查语句,代码发布之后,会自动移除
- 关键字,assert
- 一般用不到