目录
1.继承
在Java中,类的继承是指在一个现有类的基础上去构建一个新的类,构建出来的新类被称作子类,现有类被称作父类或基类,子类会自动拥有父类所有可继承的属性和方法。
如果想声明一个类继承另一个类,需要使用extends关键字
[修饰符] class 子类名 extends 父类名 { // 程序核心代码 }
类的修饰符是可选的,用来指定类的访问权限,可以使用public或者省略不写;子类名和父类名都是必选的,并且子类与父类之间要使用extends关键字实现继承关系。
注意:
-
在Java中,类只支持单继承,不允许多重继承,也就是说一个类只能有一个直接父类
-
多个类可以继承同一个父类
-
在Java中,多层继承是可以的,即一个类的父类可以再去继承另外的父类
-
在Java中,子类和父类是一种相对概念,也就是说,一个类是某个类父类的同时,也可以是另一个类的子类
子类会自动继承父类中公共的方法,但有时在子类中需要对继承的方法进行一些修改,即对父类的方法进行重写。需要注意的是,子类中重写的方法需要和父类被重写的方法具有相同的方法名、参数列表以及返回值类型
使用super关键字调用父类的成员变量
super.成员变量
使用super关键字调用父类的成员方法
super.成员方法([参数1,参数2...])
使用super关键字调用父类的构造方法
super([参数1,参数2...])
在Java中提供了一个Object类,它是所有类的父类,即每个类都直接或间接继承自该类,因此,Object类通常被称之为超类、基类或根类。
方法声明 | 功能描述 |
---|---|
boolean equals(Object) | 判断某个对象与此对象是否相等 |
final Class<?> getClass() | 返回此Object的运行时类 |
int hashCode() | 返回该对象的哈希码值 |
String toString() | 返回该对象的字符串表示 |
void finalize() | 垃圾回收器调用此方法来清理没有被任何引用变量所引用对象的资源 |
2.Final
-
Java中的类被final关键字修饰后,该类将不可以被继承,也就是不能够派生子类。
-
当一个类的方法被final关键字修饰后,这个类的子类将不能重写该方法。
-
Java中被final修饰的变量被称之为常量,它只能被赋值一次,也就是说final修饰的变量一旦被赋值,其值不能改变。
3.抽象类和接口
抽象类
当定义一个类时,常常需要定义一些方法来描述该类的行为特征,但有时这些方法的实现方式是无法确定的。针对上述这种情况,Java提供了抽象方法来满足这种需求。抽象方法必须使用abstract关键字来修饰,并且在定义方法时不需要实现方法体。当一个类中包含了抽象方法,那么该类也必须使用abstract关键字来修饰,这种使用abstract关键字修饰的类就是抽象类。
// 定义抽象类 [修饰符] abstract class 类名 { // 定义抽象方法 [修饰符] abstract 方法返回值类型 方法名([参数列表]); // 其他方法或属性 }
需要注意的是,包含抽象方法的类必须定义为抽象类,但抽象类中可以不包含任何抽象方法。另外,抽象类是不可以被实例化的,因为抽象类中有可能包含抽象方法,抽象方法是没有方法体的,不可以被调用。如果想调用抽象类中定义的抽象方法,需要创建一个子类,在子类中实现抽象类中的抽象方法
小提示:
定义抽象方法只需要在普通方法上增加abstract关键字,并把普通方法的方法体(花括号以及花括号中的部分)全部去掉,然后在方法名称后增加英文分号即可,例如public abstract void shout();
接口
如果一个抽象类中的所有方法都是抽象的,则可以将这个类定义为Java中的另一种形式——接口。接口是一种特殊的抽象类,它不能包含普通方法,其内部的所有方法都是抽象方法,它将抽象进行的更为彻底。
与定义类不同的是,在定义接口时,不再使用class关键字,而是使用interface关键字来声明。
[修饰符] interface 接口名 [extends 父接口1,父接口2,...] { [public] [static] [final] 常量类型 常量名 = 常量值; [public] [abstract] 方法返回值类型 方法名([参数列表]); [public] default 方法返回值类型 方法名([参数列表]){ // 默认方法的方法体 } [public] static 方法返回值类型 方法名([参数列表]){ // 类方法的方法体 } }
“[]”中的内容都是可选的,修饰符可以使用public或直接省略(省略时默认采用包权限访问控制符);“extends 父接口1,父接口2,...”表示定义一个接口时,可以同时继承多个父接口,这也是为了解决类的单继承的限制;在接口内部可以定义多个常量和抽象方法,定义常量时必须进行初始化赋值,定义默认方法和静态方法时,可以有方法体。
定义接口的实现类语法格式如下:
[修饰符] class 类名 [extends 父类名] [implements 接口1,接口2,...] { ... }
接口的特点
(1)在JDK 8之前,接口中的方法都必须是抽象的,并且方法不能包含方法体。在调用抽象方法时,必须通过接 口的实现类的对象才能调用实现方法;从JDK 8开始,接口中的方法除了包含抽象方法外,还包含默认方法和 静态方法,默认方法和静态方法都可以有方法体,并且静态方法可以直接通过“接口.方法名”来调用。
(2)当一个类实现接口时,如果这个类是抽象类,只需实现接口中的部分抽象方法即可,否则需要实现接口中的 所有抽象方法。
(3)一个类可以通过implements关键字同时实现多个接口,被实现的多个接口之间要用英文逗号(,)隔开。
(4)接口之间可以通过extends关键字实现继承,并且一个接口可以同时继承多个接口,接口之间用英文逗号 (,)隔开。
(5)一个类在继承一个类的同时还可以实现接口,此时,extends关键字必须位于implements关键字之前
4.多态
多态是指不同类的对象在调用同一个方法时所呈现出的多种不同行为。通常来说,在一个类中定义的属性和方法被其他类继承或重写后,当把子类对象直接赋值给父类引用变量时,相同引用类型的变量调用同一个方法所呈现出的多种不同形态。通过多态,消除了类之间的耦合关系,大大提高了程序的可扩展性和可维护性。
Java的多态性是由类的继承、方法重写以及父类引用指向子类对象体现的。由于一个父类可以有多个子类,多个子类都可以重写父类方法,并且多个不同的子类对象也可以指向同一个父类。这样,程序只有在运行时程序才能知道具体代表的是哪个子类对象,这就体现了多态性。
-
向上转型:父类引用可以调用子类中重写父类的方法,这样当需要添加新功能时,新增一个子类即可,而不需要更改父类代码;
Animal animal = new Dog(); animal.eat(); // 会调用Dog类中的eat方法; animal = new Cat(); animal.eat(); // 又会调用Cat类中的eat方法;
-
向下转型:一般是为了重新获得因为向上转型而失去的子类特性,所以向下转型前一般都有向上转型,也会结合instanceOf使用;
Animal animal = new Dog(); Dog dog = (Dog) animal; // 为了使用Dog类中独有的属性和方法 animal = new Cat(); Cat cat = (Cat) animal; // 为了使用Cat类中独有的属性和方法
java中要实现多态,必须要满足如下几个条件,缺一不可:
1.必须在继承体系下
2.子类必须要对父类方法进行重写
3.通过父类的引用调用重写方法
区别 | 重写(override) | 重载(override) |
---|---|---|
参数列表 | 一定不能修改 | 必须修改 |
返回类型 | 一定不能修改【除非可以构成父子关系】 | 可以修改 |
访问限定符 | 一定不能做更严格的限定【可以降低限定】 | 可以修改 |
5.内部类
在一个类中除了可以定义成员变量、成员方法,还可以定义类,这样的类被称作成员内部类。在成员内部类中,可以访问外部类的所有成员,包括成员变量和成员方法;在外部类中,同样可以访问成员内部类的变量和方法。
创建内部类对象的具体语法格式如下:
外部类名.内部类名 变量名 = new 外部类名().new 内部类名();
局部内部类,也叫做方法内部类,就是定义在某个局部范围中的类,它和局部变量一样,都是在方法中定义的,其有效范围只限于方法内部。
在局部内部类中,局部内部类可以访问外部类的所有成员变量和方法,而局部内部类中的变量和方法却只能在创建该局部内部类的方法中进行访问。
静态内部类,就是使用static关键字修饰的成员内部类。与成员内部类相比,在形式上,静态内部类只是在内部类前增加了static关键字,但在功能上,静态内部类中只能访问外部类的静态成员,同时通过外部类访问静态内部类成员时,可以跳过外部类从而直接通过内部类访问静态内部类成员。
创建静态内部类对象的基本语法格式如下:
外部类名.静态内部类名 变量名 = new 外部类名.静态内部类名();
匿名内部类其实就是没有名称的内部类,在调用包含有接口类型参数的方法时,通常为了简化代码,不会创建一个接口的实现类作为方法参数传入,而是直接通过匿名内部类的形式传入一个接口类型参数,在匿名内部类中直接完成方法的实现。
创建匿名内部类的基本语法格式如下:
new 父接口(){ // 匿名内部类实现部分 }
6.JDK8-Lambda表达式
如果匿名内部类的实现非常简单,例如只包含一个抽象方法的接口,那么匿名内部类的语法仍然显得比较冗余。为此,JDK 8中新增了一个特性Lambda表达式,这种表达式只针对有一个抽象方法的接口实现,以简洁的表达式形式实现接口功能来作为方法参数。
一个Lambda表达式由三个部分组成,分别为参数列表、“->”和表达式主体,其语法格式如下:
([数据类型 参数名,数据类型 参数名,...]) -> {表达式主体}
从上述语法格式上看,Lambda表达式的书写非常简单,下面针对Lambda表达式的组成部分进行简单介绍,具体如下:
(1)([数据类型 参数名,数据类型 参数名,...]):用来向表达式主体传递接口方法需要的参数,多个参数名中间必须用英文逗号“,”进行分隔;在编写Lambda表达式时,可以省略参数的数据类型,后面的表达式主体会自动进行校对和匹配;同时,如果只有一个参数,则可以省略括号“()”。
(2)->:表示Lambda表达式箭牌,用来指定参数数据指向,不能省略,且必须用英文横线和大于号书写。
(3){表达式主体}:由单个表达式或语句块组成的主体,本质就是接口中抽象方法的具体实现,如果表达式主体只有一条语句,那么可以省略包含主体的大括号;另外,在Lambda表达式主体中允许有返回值,当只有一条return语句时,也可以省略return关键字。
接口中有且只有一个抽象方法时才能使用Lamdba表达式代替匿名内部类。这是因为Lamdba表达式是基于函数式接口实现的,所谓函数式接口是指有且仅有一个抽象方法的接口,Lambda表达式就是Java中函数式编程的体现,只有确保接口中有且仅有一个抽象方法,Lambda表达式才能顺利地推导出所实现的这个接口中的方法。
在JDK 8中,专门为函数式接口引入了一个@FunctionalInterface注解,该注解只是显示的标注了接口是一个函数式接口,并强制编辑器进行更严格的检查,确保该接口是函数式接口,如果不是函数式接口,那么编译器就会报错,而对程序运行并没有实质上的影响,
种类 | Lambda表达式示例 | 对应的引用示例 |
---|---|---|
类名引用普通方法 | (x,y,...)-> 对象名x.类普通方法名(y,...) | 类名::类普通方法名 |
类名引用静态方法 | (x,y,...) -> 类名.类静态方法名(x,y,...) | 类名::类静态方法名 |
对象名引用方法 | (x,y,...) -> 对象名.实例方法名(x,y,...) | 对象名::实例方法名 |
构造器引用 | (x,y,...) -> new 类名 (x,y,...) | 类名::new |
7.异常
在程序运行的过程中,也会发生这种非正常状况,例如程序运行时磁盘空间不足、网络连接中断、被加载的类不存在等。针对这些非正常情况,Java语言中引入了异常,以异常类的形式对这些非正常情况进行封装,并通过异常处理机制对程序运行时发生的各种问题进行处理。
Throwable类中的常用方法
方法声明 | 功能描述 |
---|---|
String getMessage() | 返回此throwable的详细消息字符串 |
void printStackTrace() | 将此throwable及其追踪输出至标准错误流 |
void printStackTrace(PrintStream s) | 将此throwable及其追踪输出到指定的输出流 |
● Error类称为错误类,它表示Java运行时产生的系统内部错误或资源耗尽的错误,是比较严重的,仅靠修改程序本身是不能恢复执行的,例如系统崩溃,虚拟机错误等。
● Exception类称为异常类,它表示程序本身可以处理的错误。在Java程序开发中进行的异常处理,都是针对Excption类及其子类的。在Exception类的众多子类中有一个特殊的RuntimeException类,该类及其子类用于表示运行时异常。除了此类,Exception类下所有其他的子类都用于表示编译时异常。
异常的类型
1.编译时异常
在Exception的子类中,除了RuntimeException类及其子类外,其他子类都是编译时异常。编译时异常的特点是在程序编写过程中,Java编译器就会对编写的代码进行检查,如果出现比较明显的异常就必须对异常进行处理,否则程序无法通过编译。
处理编译时异常有两种方式,具体如下:
● 使用try…catch语句对异常进行捕获处理;
● 使用throws关键字声明抛出异常,让调用者对其处理。
2.运行时异常
RuntimeException类及其子类都是运行时异常。运行时异常是在程序运行时由Java虚拟机自动进行捕获处理的,即使没有使用try..catch语句捕获或使用throws关键字声明抛出,程序也能编译通过,只是在运行过程中可能报错。
try...catch 异常捕获
当程序发生异常时,会立即终止,无法继续向下执行。为了保证程序能够有效的执行,Java中提供了一种对异常进行处理的方式——异常捕获。
异常捕获通常使用try…catch语句,其具体语法格式如下:
try { // 可能发生异常的语句 } catch(Exception类或其子类 e){ // 对捕获的异常进行相应处理 }
有时候会希望有些语句无论程序是否发生异常都要执行,这时就可以在try…catch语句后,加一个finally{}代码块。
public class ExceptionDemo { public static int divide(int x, int y) { try { int result = x / y; // 定义一个变量result记录两个数相除的结果 return result; } catch (Exception e) { // 对异常进行捕获处理8 System.out.println("捕获的异常信息为:" + e.getMessage()); } finally { System.out.println("执行finally代码块,无论程序是否异常,都会执行"); } return -1; } public static void main(String[] args) { int result = divide(4, 0); if(result == -1){ System.out.println("程序发生异常!"); }else{ System.out.println(result); } } }
有些时候,方法中代码是否会出现异常,开发者并不明确或者并不急于处理,为此,Java允许将这种异常从当前方法中抛出,然后让后续的调用者在使用时再进行异常处理。
在Java中,将异常抛出需要使用throws关键字来实现,该关键字用在会抛出异常的方法名称后,同时支持一次性抛出多种类型的异常,基本语法格式如下:
[修饰符] 返回值类型 方法名([参数类型 参数名1...]) throws 异常类1,异常类2,... { // 方法体... }
从上述语法格式中可以看出,throws关键字需要写在方法声明的后面,throws后面需要声明方法中发生异常的类型,通常将这种做法称为方法声明抛出一个异常。
public class Example{ // 下面的方法实现了两个整数相除,并使用throws关键字声明抛出异常 public static int divide(int x, int y) throws Exception { int result = x / y; return result; } public static void main(String[] args) throws Exception { int result = divide(4, 0); System.out.println(result); } }
除了可以通过throws关键字抛出异常外,还可以使用throw关键字抛出异常。与throws有所不同的是,throw用于方法体内,并且抛出的是一个异常类对象,而throws关键字用在方法声明中,用来指明方法可能抛出的多个异常。
通过throw关键字抛出异常后,还需要使用throws关键字或try…catch对异常进行处理。需要注意的是,如果throw抛出的是Error、RuntimeException或它们的子类异常对象,则无需使用throws关键字或try…catch对异常进行处理。
使用throw关键字抛出异常的语法格式如下:
[修饰符] 返回值类型 方法名([参数类型 参数名,...]) throw 抛出的异常类 { // 方法体... throw new Exception类或其子类构造方法; }
Java允许用户自定义异常,但自定义的异常类必须继承自Exception或其子类。
先自定义一个异常类
public class DivideByMinusException extends Exception{ public DivideByMinusException (){ super(); // 调用Exception无参的构造方法 } public DivideByMinusException (String message){ super(message); // 调用Exception有参的构造方法 } }
测试类
public class Demo { // 下面的方法实现了两个整数相除, public static int divide(int x,int y) throws DivideByMinusException { if (y == 0) { throw new DivideByMinusException("除数是0"); } int result = x / y; return result; } public static void main(String[] args) { try { int result = divide(4, 0); System.out.println(result); } catch (DivideByMinusException e) { System.out.println("捕获的异常信息为:" + e.getMessage()); } } }
运行结果
8.垃圾回收
在Java中,当一个对象成为垃圾后仍会占用内存空间,时间一长,就会导致内存空间的不足。针对这种情况,Java中引入了垃圾回收机制(Java GC)
当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以把它所处的状态分成如下三种:
● 可用状态:当一个对象被创建后,如果有一个以上的引用变量引用它,那么这个对象在程序中将处于可用状态,程序可以通过引用变量来调用该对象的实例变量和方法。
● 可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它就进入了可恢复状态。在这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复状态对象的finalize()方法进行资源清理。如果系统在调用finalize()方法前重新使一个引用变量引用该对象,则这个对象会再次变为可用状态;否则该对象将进入不可用状态。
● 不可用状态:当对象失去了所有引用变量的关联,且系统已经调用所有对象的finalize()方法后依然没有使该对象变成可用状态,那么这个对象将永久的失去引用,变成不可用状态。只有当一个对象处于不可用状态时,系统才会真正的回收该对象所占用的内存空间。
除了等待Java虚拟机进行自动垃圾回收外,还可以通过如下两种方式强制系统进行垃圾回收。
● 调用System类的gc()静态方法:System.gc()。
● 调用Runtime对象的gc()实例方法:Runtime.getRuntime().gc()。
实际上,调用System.gc()方法时,所执行的也是Runtime.getRuntime().gc()方法。需要说明的是,调用这两种方式可以强制启动垃圾回收器进行垃圾回收,但系统是否立即进行垃圾回收依然具有不确定性。大多数情况下,强制系统垃圾回收后总是有一定的效果。
当一个对象在内存中被释放时,它的finalize()方法会被自动调用,finalize()方法是定义在Object类中的实例方法,其方法原型如下:
protected void finalize() throws Throwable { }
任何Java类都可以重写Object类的finalize()方法,在该方法中清理该对象占用的资源。如果程序终止之前仍然没有进行垃圾回收,则不会调用失去引用对象的finalize()方法来清理资源。
class Person { // 下面定义的finalize()方法会在垃圾回收前被调用 public void finalize() { System.out.println("对象将被作为垃圾回收..."); } } public class Example { // 1、演示一个不通知强制垃圾回收的方法 public static void recyclegWaste1(){ Person p1 = new Person(); p1 = null; int i = 1; while (i < 5) { System.out.println("方法1循环中..........."); i++; } } // 2、演示一个通知强制垃圾回收的方法 public static void recyclegWaste2(){ Person p2 = new Person(); p2 = null; // 通知垃圾回收器进行强制垃圾回收 System.gc(); // Runtime.getRuntime().gc(); int i = 1; while (i < 5) { System.out.println("方法2循环中..........."); i++; } } public static void main(String[] args) { // 分别调用两个模拟演示垃圾回收的方法 recyclegWaste1(); System.out.println("================"); recyclegWaste2(); } }