1 接口(interface)
1.1 概述
接口(Interface)在计算机科学中,特别是在面向对象编程(OOP)中,是一个重要的概念。它定义了一组方法的规范,但没有实现这些方法的具体代码。接口的主要目的是确保类遵循特定的协议,从而允许不同的类之间进行交互,即使它们的内部实现可能完全不同。
1.2 定义格式
接口的定义,它与定义类方式相似,但是使用 interface
关键字。它也会被编译成.class文件,但一定要明确它并不是类,而是另外一种引用数据类型。
引用数据类型:数组,类,枚举,接口,注解。
1.2.1 接口的声明格式
[修饰符] interface 接口名{ //接口的成员列表: // 公共的静态常量 // 公共的抽象方法 // 公共的默认方法(JDK1.8以上) // 公共的静态方法(JDK1.8以上) // 私有方法(JDK1.9以上) }
示例代码:
public interface USB3{ //静态常量 long MAX_SPEED = 500*1024*1024;//500MB/s //抽象方法 void in(); void out(); //默认方法 default void start(){ System.out.println("开始"); } default void stop(){ System.out.println("结束"); } //静态方法 static void show(){ System.out.println("USB 3.0可以同步全速地进行读写操作"); } }
1.2.2 接口的成员说明
在JDK8.0 之前,接口中只允许出现:
(1)公共的静态的常量:其中public static final
可以省略
(2)公共的抽象的方法:其中public abstract
可以省略
理解:接口是从多个相似类中抽象出来的规范,不需要提供具体实现
在JDK8.0 时,接口中允许声明默认方法
和静态方法
:
(3)公共的默认的方法:其中public 可以省略,建议保留,但是default不能省略
(4)公共的静态的方法:其中public 可以省略,建议保留,但是static不能省略
在JDK9.0 时,接口又增加了:
(5)私有方法
除此之外,接口中没有构造器,没有初始化块,因为接口中没有成员变量需要动态初始化。
1.3 接口的使用规则
1、类实现接口(implements)
接口不能创建对象,但是可以被类实现(implements
,类似于被继承)。
类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements
关键字。
【修饰符】 class 实现类 implements 接口{ // 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写 // 重写接口中默认方法【可选】 } 【修饰符】 class 实现类 extends 父类 implements 接口{ // 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写 // 重写接口中默认方法【可选】 }
注意:
-
如果接口的实现类是非抽象类,那么必须
重写接口中所有抽象方法
。 -
默认方法可以选择保留,也可以重写。
重写时,default单词就不要再写了,它只用于在接口中表示默认方法,到类中就没有默认方法的概念了
-
接口中的静态方法不能被继承也不能被重写
举例:
interface USB{ // public void start() ; public void stop() ; } class Computer{ public static void show(USB usb){ usb.start() ; System.out.println("=========== USB 设备工作 ========") ; usb.stop() ; } }; class Flash implements USB{ public void start(){ // 重写方法 System.out.println("U盘开始工作。") ; } public void stop(){ // 重写方法 System.out.println("U盘停止工作。") ; } }; class Print implements USB{ public void start(){ // 重写方法 System.out.println("打印机开始工作。") ; } public void stop(){ // 重写方法 System.out.println("打印机停止工作。") ; } }; public class InterfaceDemo{ public static void main(String args[]){ Computer.show(new Flash()) ; Computer.show(new Print()) ; c.show(new USB(){ public void start(){ System.out.println("移动硬盘开始运行"); } public void stop(){ System.out.println("移动硬盘停止运行"); } }); } };
2、接口的多实现(implements)
之前学过,在继承体系中,一个类只能继承一个父类。而对于接口而言,一个类是可以实现多个接口的,这叫做接口的多实现
。并且,一个类能继承一个父类,同时实现多个接口。
实现格式:
【修饰符】 class 实现类 implements 接口1,接口2,接口3。。。{ // 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写 // 重写接口中默认方法【可选】 } 【修饰符】 class 实现类 extends 父类 implements 接口1,接口2,接口3。。。{ // 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写 // 重写接口中默认方法【可选】 }
接口中,有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次。
举例:
定义多个接口:
public interface A { void showA(); }
public interface B { void showB(); }
定义实现类:
public class C implements A,B { @Override public void showA() { System.out.println("showA"); } @Override public void showB() { System.out.println("showB"); } }
测试类
public class TestC { public static void main(String[] args) { C c = new C(); c.showA(); c.showB(); } }
3、接口的多继承(extends)
一个接口能继承另一个或者多个接口,接口的继承也使用 extends
关键字,子接口继承父接口的方法。
定义父接口:
public interface Chargeable { void charge(); void in(); void out(); }
定义子接口:
public interface UsbC extends Chargeable,USB3 { void reverse(); }
定义子接口的实现类:
public class TypeCConverter implements UsbC { @Override public void reverse() { System.out.println("正反面都支持"); } @Override public void charge() { System.out.println("可充电"); } @Override public void in() { System.out.println("接收数据"); } @Override public void out() { System.out.println("输出数据"); } }
所有父接口的抽象方法都有重写。
方法签名相同的抽象方法只需要实现一次。
4、接口与实现类对象构成多态引用
实现类实现接口,类似于子类继承父类,因此,接口类型的变量与实现类的对象之间,也可以构成多态引用。通过接口类型的变量调用方法,最终执行的是你new的实现类对象实现的方法体。
接口的不同实现类:
public class Mouse implements USB3 { @Override public void out() { System.out.println("发送脉冲信号"); } @Override public void in() { System.out.println("不接收信号"); } }
public class KeyBoard implements USB3{ @Override public void in() { System.out.println("不接收信号"); } @Override public void out() { System.out.println("发送按键信号"); } }
测试类
public class TestComputer { public static void main(String[] args) { Computer computer = new Computer(); USB3 usb = new Mouse(); computer.setUsb(usb); usb.start(); usb.out(); usb.in(); usb.stop(); System.out.println("--------------------------"); usb = new KeyBoard(); computer.setUsb(usb); usb.start(); usb.out(); usb.in(); usb.stop(); System.out.println("--------------------------"); usb = new MobileHDD(); computer.setUsb(usb); usb.start(); usb.out(); usb.in(); usb.stop(); } }
5、使用接口的静态成员
接口不能直接创建对象,但是可以通过接口名直接调用接口的静态方法和静态常量。
public class TestUSB3 { public static void main(String[] args) { //通过“接口名.”调用接口的静态方法 (JDK8.0才能开始使用) USB3.show(); //通过“接口名.”直接使用接口的静态常量 System.out.println(USB3.MAX_SPEED); } }
6、使用接口的非静态方法
-
对于接口的静态方法,直接使用“
接口名.
”进行调用即可-
也只能使用“接口名."进行调用,不能通过实现类的对象进行调用
-
-
对于接口的抽象方法、默认方法,只能通过实现类对象才可以调用
-
接口不能直接创建对象,只能创建实现类的对象
-
public class TestMobileHDD { public static void main(String[] args) { //创建实现类对象 MobileHDD b = new MobileHDD(); //通过实现类对象调用重写的抽象方法,以及接口的默认方法,如果实现类重写了就执行重写的默认方法,如果没有重写,就执行接口中的默认方法 b.start(); b.in(); b.stop(); //通过接口名调用接口的静态方法 // MobileHDD.show(); // b.show(); Usb3.show(); } }
1.4 JDK8中相关冲突问题
1.4.1 默认方法冲突问题
(1)类优先原则
当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的抽象方法重名,子类就近选择执行父类的成员方法。代码如下:
定义接口:
public interface Friend { default void date(){//约会 System.out.println("吃喝玩乐"); } }
定义父类:
public class Father { public void date(){//约会 System.out.println("爸爸约吃饭"); } }
定义子类:
public class Son extends Father implements Friend { @Override public void date() { //(1)不重写默认保留父类的 //(2)调用父类被重写的 // super.date(); //(3)保留父接口的 // Friend.super.date(); //(4)完全重写 System.out.println("跟康师傅学Java"); } }
定义测试类:
public class TestSon { public static void main(String[] args) { Son s = new Son(); s.date(); } }
(2)接口冲突(左右为难)
-
当一个类同时实现了多个父接口,而多个父接口中包含方法签名相同的默认方法时,怎么办呢?
无论你多难抉择,最终都是要做出选择的。
声明接口:
public interface BoyFriend { default void date(){//约会 System.out.println("神秘约会"); } }
选择保留其中一个,通过“接口名.super.方法名
"的方法选择保留哪个接口的默认方法。
public class Girl implements Friend,BoyFriend{ @Override public void date() { //(1)保留其中一个父接口的 // Friend.super.date(); // BoyFriend.super.date(); //(2)完全重写 System.out.println("跟康师傅学Java"); } }
测试类
public class TestGirl { public static void main(String[] args) { Girl girl = new Girl(); girl.date(); } }
-
当一个子接口同时继承了多个接口,而多个父接口中包含方法签名相同的默认方法时,怎么办呢?
另一个父接口:
public interface USB2 { //静态常量 long MAX_SPEED = 60*1024*1024;//60MB/s //抽象方法 void in(); void out(); //默认方法 public default void start(){ System.out.println("开始"); } public default void stop(){ System.out.println("结束"); } //静态方法 public static void show(){ System.out.println("USB 2.0可以高速地进行读写操作"); } }
子接口:
public interface USB extends USB2,USB3 { @Override default void start() { System.out.println("Usb.start"); } @Override default void stop() { System.out.println("Usb.stop"); } }
小贴士:
子接口重写默认方法时,default关键字可以保留。
子类重写默认方法时,default关键字不可以保留。
1.4.2 常量冲突问题
-
当子类继承父类又实现父接口,而父类中存在与父接口常量同名的成员变量,并且该成员变量名在子类中仍然可见。
-
当子类同时实现多个接口,而多个接口存在相同同名常量。
此时在子类中想要引用父类或父接口的同名的常量或成员变量时,就会有冲突问题。
父类和父接口:
public class SuperClass { int x = 1; }
public interface SuperInterface { int x = 2; int y = 2; }
public interface MotherInterface { int x = 3; }
子类:
public class SubClass extends SuperClass implements SuperInterface,MotherInterface { public void method(){ // System.out.println("x = " + x);//模糊不清 System.out.println("super.x = " + super.x); System.out.println("SuperInterface.x = " + SuperInterface.x); System.out.println("MotherInterface.x = " + MotherInterface.x); System.out.println("y = " + y);//没有重名问题,可以直接访问 } }
1.5 接口的总结与面试题
-
接口本身不能创建对象,只能创建接口的实现类对象,接口类型的变量可以与实现类对象构成多态引用。
-
声明接口用interface,接口的成员声明有限制:
-
(1)公共的静态常量
-
(2)公共的抽象方法
-
(3)公共的默认方法(JDK8.0 及以上)
-
(4)公共的静态方法(JDK8.0 及以上)
-
(5)私有方法(JDK9.0 及以上)
-
-
类可以实现接口,关键字是implements,而且支持多实现。如果实现类不是抽象类,就必须实现接口中所有的抽象方法。如果实现类既要继承父类又要实现父接口,那么继承(extends)在前,实现(implements)在后。
-
接口可以继承接口,关键字是extends,而且支持多继承。
-
接口的默认方法可以选择重写或不重写。如果有冲突问题,另行处理。子类重写父接口的默认方法,要去掉default,子接口重写父接口的默认方法,不要去掉default。
-
接口的静态方法不能被继承,也不能被重写。接口的静态方法只能通过“接口名.静态方法名”进行调用。
面试题
1、为什么接口中只能声明公共的静态的常量?
因为接口是标准规范,那么在规范中需要声明一些底线边界值,当实现者在实现这些规范时,不能去随意修改和触碰这些底线,否则就有“危险”。
例如:USB1.0规范中规定最大传输速率是1.5Mbps,最大输出电流是5V/500mA
USB3.0规范中规定最大传输速率是5Gbps(500MB/s),最大输出电流是5V/900mA
例如:尚硅谷学生行为规范中规定学员,早上8:25之前进班,晚上21:30之后离开等等。
2、为什么JDK8.0 之后允许接口定义静态方法和默认方法呢?因为它违反了接口作为一个抽象标准定义的概念。
静态方法
:因为之前的标准类库设计中,有很多Collection/Colletions或者Path/Paths这样成对的接口和类,后面的类中都是静态方法,而这些静态方法都是为前面的接口服务的,那么这样设计一对API,不如把静态方法直接定义到接口中使用和维护更方便。
默认方法
:(1)我们要在已有的老版接口中提供新方法时,如果添加抽象方法,就会涉及到原来使用这些接口的类就会有问题,那么为了保持与旧版本代码的兼容性,只能允许在接口中定义默认方法实现。比如:Java8中对Collection、List、Comparator等接口提供了丰富的默认方法。(2)当我们接口的某个抽象方法,在很多实现类中的实现代码是一样的,此时将这个抽象方法设计为默认方法更为合适,那么实现类就可以选择重写,也可以选择不重写。
3、为什么JDK1.9要允许接口定义私有方法呢?因为我们说接口是规范,规范是需要公开让大家遵守的。
私有方法:因为有了默认方法和静态方法这样具有具体实现的方法,那么就可能出现多个方法由共同的代码可以抽取,而这些共同的代码抽取出来的方法又只希望在接口内部使用,所以就增加了私有方法。
1.6 接口与抽象类之间的对比
在开发中,常看到一个类不是去继承一个已经实现好的类,而是要么继承抽象类,要么实现接口。
1.7 练习
笔试题:排错
interface A { int x = 0; } class B { int x = 1; } class C extends B implements A { public void pX() { System.out.println(x); } public static void main(String[] args) { new C().pX(); } }
笔试题:排错
interface Playable { void play(); } interface Bounceable { void play(); } interface Rollable extends Playable, Bounceable { Ball ball = new Ball("PingPang"); } class Ball implements Rollable { private String name; public String getName() { return name; } public Ball(String name) { this.name = name; } public void play() { ball = new Ball("Football"); System.out.println(ball.getName()); } }
练习1:
定义一个接口用来实现两个对象的比较。
interface CompareObject{ //若返回值是 0 , 代表相等; 若为正数,代表当前对象大;负数代表当前对象小 public int compareTo(Object o); }
定义一个Circle类,声明redius属性,提供getter和setter方法
定义一个ComparableCircle类,继承Circle类并且实现CompareObject接口。在ComparableCircle类中给出接口中方法compareTo的实现体,用来比较两个圆的半径大小。
定义一个测试类InterfaceTest,创建两个ComparableCircle对象,调用compareTo方法比较两个类的半径大小。
思考:参照上述做法定义矩形类Rectangle和ComparableRectangle类,在ComparableRectangle类中给出compareTo方法的实现,比较两个矩形的面积大小。
public class Circle { private double radius;//半径 public Circle() { } public Circle(double radius) { this.radius = radius; } public double getRadius() { return radius; } public void setRadius(double radius) { this.radius = radius; } @Override public String toString() { return "Circle{" + "radius=" + radius + '}'; } }
public class ComparableCircle extends Circle implements CompareObject{ public ComparableCircle() { } public ComparableCircle(double radius) { super(radius); } //根据对象的半径的大小,比较对象的大小 @Override public int compareTo(Object o) { if(this == o){ return 0; } if(o instanceof ComparableCircle){ ComparableCircle c = (ComparableCircle)o; //错误的 // return (int) (this.getRadius() - c.getRadius()); //正确的写法1: // if(this.getRadius() > c.getRadius()){ // return 1; // }else if(this.getRadius() < c.getRadius()){ // return -1; // }else{ // return 0; // } //正确的写法2: return Double.compare(this.getRadius(),c.getRadius()); }else{ return 2; //如果输入的类型不匹配,则返回2 // throw new RuntimeException("输入的类型不匹配"); } } }
public interface CompareObject { //若返回值是 0 , 代表相等; 若为正数,代表当前对象大;负数代表当前对象小 public int compareTo(Object o); }
public class InterfaceTest { public static void main(String[] args) { ComparableCircle c1 = new ComparableCircle(2.3); ComparableCircle c2 = new ComparableCircle(5.3); int compareValue = c1.compareTo(c2); if(compareValue > 0){ System.out.println("c1对象大"); }else if(compareValue < 0){ System.out.println("c2对象大"); }else{ System.out.println("c1和c2一样大"); } } }
练习2:交通工具案例
阿里的一个工程师,声明的属性和方法如下:
其中,有一个乘坐交通工具的方法takingVehicle(),在此方法中调用交通工具的run()。为了出行方便,他买了一辆捷安特自行车、一辆雅迪电动车和一辆奔驰轿车。这里涉及到的相关类及接口关系如下:
其中,电动车增加动力的方式是充电,轿车增加动力的方式是加油。在具体交通工具的run()中调用其所在类的相关属性信息。
请编写相关代码,并测试。
提示:创建Vehicle[]数组,保存阿里工程师的三辆交通工具,并分别在工程师的takingVehicle()中调用。
public class Bicycle extends Vehicle{ public Bicycle() { } public Bicycle(String brand, String color) { super(brand, color); } @Override public void run() { System.out.println("自行车通过人力脚蹬行驶"); } }
public class Car extends Vehicle implements IPower{ private String carNumber; public Car() { } public Car(String brand, String color, String carNumber) { super(brand, color); this.carNumber = carNumber; } public String getCarNumber() { return carNumber; } public void setCarNumber(String carNumber) { this.carNumber = carNumber; } @Override public void run() { System.out.println("汽车通过内燃机驱动行驶"); } @Override public void power() { System.out.println("汽车通过汽油提供动力"); } }
public class Developer { private String name; private int age; public Developer() { } public Developer(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public void takingVehicle(Vehicle vehicle){ vehicle.run(); } }
public class ElectricVehicle extends Vehicle implements IPower{ public ElectricVehicle() { } public ElectricVehicle(String brand, String color) { super(brand, color); } @Override public void run() { System.out.println("电动车通过电机驱动行驶"); } @Override public void power() { System.out.println("电动车使用电力提供动力"); } }
public interface IPower { void power(); }
public abstract class Vehicle { private String brand;//品牌 private String color;//颜色 public Vehicle() { } public Vehicle(String brand, String color) { this.brand = brand; this.color = color; } public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } public abstract void run(); }
public class VehicleTest { public static void main(String[] args) { Developer developer = new Developer(); //创建三个交通工具,保存在数组中 Vehicle[] vehicles = new Vehicle[3]; vehicles[0] = new Bicycle("捷安特","骚红色"); vehicles[1] = new ElectricVehicle("雅迪","天蓝色"); vehicles[2] = new Car("奔驰","黑色","沪Au888"); for (int i = 0;i < vehicles.length;i++){ developer.takingVehicle(vehicles[i]); if(vehicles[i] instanceof IPower){ ((IPower) vehicles[i]).power(); } } } }
2 枚举类
2.1 概述
-
枚举类型本质上也是一种类,只不过是这个类的对象是有限的、固定的几个,不能让用户随意创建。
-
枚举类的例子举不胜举:
-
星期
:Monday(星期一)......Sunday(星期天) -
性别
:Man(男)、Woman(女) -
月份
:January(1月)......December(12月) -
季节
:Spring(春节)......Winter(冬天) -
三原色
:red(红色)、green(绿色)、blue(蓝色) -
支付方式
:Cash(现金)、WeChatPay(微信)、Alipay(支付宝)、BankCard(银行卡)、CreditCard(信用卡) -
就职状态
:Busy(忙碌)、Free(空闲)、Vocation(休假)、Dimission(离职) -
订单状态
:Nonpayment(未付款)、Paid(已付款)、Fulfilled(已配货)、Delivered(已发货)、Checked(已确认收货)、Return(退货)、Exchange(换货)、Cancel(取消) -
线程状态
:创建、就绪、运行、阻塞、死亡
-
-
若枚举只有一个对象, 则可以作为一种单例模式的实现方式。
-
枚举类的实现:
-
在JDK5.0 之前,需要程序员自定义枚举类型。
-
在JDK5.0 之后,Java支持
enum
关键字来快速定义枚举类型。
-
2.2 定义枚举类(JDK5.0 之前)
在JDK5.0 之前如何声明枚举类呢?
-
私有化
类的构造器,保证不能在类的外部创建其对象 -
在类的内部创建枚举类的实例。声明为:
public static final
,对外暴露这些常量对象 -
对象如果有
实例变量
,应该声明为private final
(建议,不是必须),并在构造器中初始化
示例代码:
class Season{ private final String SEASONNAME;//季节的名称 private final String SEASONDESC;//季节的描述 private Season(String seasonName,String seasonDesc){ this.SEASONNAME = seasonName; this.SEASONDESC = seasonDesc; } public static final Season SPRING = new Season("春天", "春暖花开"); public static final Season SUMMER = new Season("夏天", "夏日炎炎"); public static final Season AUTUMN = new Season("秋天", "秋高气爽"); public static final Season WINTER = new Season("冬天", "白雪皑皑"); @Override public String toString() { return "Season{" + "SEASONNAME='" + SEASONNAME + '\'' + ", SEASONDESC='" + SEASONDESC + '\'' + '}'; } } class SeasonTest{ public static void main(String[] args) { System.out.println(Season.AUTUMN); } }
2.3 定义枚举类(JDK5.0 之后)
2.3.1 enum关键字声明枚举
【修饰符】 enum 枚举类名{ 常量对象列表 } 【修饰符】 enum 枚举类名{ 常量对象列表; 对象的实例变量列表; }
举例1:
public enum Week { MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY; }
public class TestEnum { public static void main(String[] args) { Season spring = Season.SPRING; System.out.println(spring); } }
2.3.2 enum方式定义的要求和特点
-
枚举类的常量对象列表必须在枚举类的首行,因为是常量,所以建议大写。
-
列出的实例系统会自动添加 public static final 修饰。
-
如果常量对象列表后面没有其他代码,那么“;”可以省略,否则不可以省略“;”。
-
编译器给枚举类默认提供的是private的无参构造,如果枚举类需要的是无参构造,就不需要声明,写常量对象列表时也不用加参数
-
如果枚举类需要的是有参构造,需要手动定义,有参构造的private可以省略,调用有参构造的方法就是在常量对象名后面加(实参列表)就可以。
-
枚举类默认继承的是java.lang.Enum类,因此不能再继承其他的类型。
-
JDK5.0 之后switch,提供支持枚举类型,case后面可以写枚举常量名,无需添加枚举类作为限定。
举例2:
public enum SeasonEnum { SPRING("春天","春风又绿江南岸"), SUMMER("夏天","映日荷花别样红"), AUTUMN("秋天","秋水共长天一色"), WINTER("冬天","窗含西岭千秋雪"); private final String seasonName; private final String seasonDesc; private SeasonEnum(String seasonName, String seasonDesc) { this.seasonName = seasonName; this.seasonDesc = seasonDesc; } public String getSeasonName() { return seasonName; } public String getSeasonDesc() { return seasonDesc; } }
举例3:
public enum Week { MONDAY("星期一"), TUESDAY("星期二"), WEDNESDAY("星期三"), THURSDAY("星期四"), FRIDAY("星期五"), SATURDAY("星期六"), SUNDAY("星期日"); private final String description; private Week(String description){ this.description = description; } @Override public String toString() { return super.toString() +":"+ description; } }
public class TestWeek { public static void main(String[] args) { Week week = Week.MONDAY; System.out.println(week); switch (week){ case MONDAY: System.out.println("怀念周末,困意很浓");break; case TUESDAY: System.out.println("进入学习状态");break; case WEDNESDAY: System.out.println("死撑");break; case THURSDAY: System.out.println("小放松");break; case FRIDAY: System.out.println("又信心满满");break; case SATURDAY: System.out.println("开始盼周末,无心学习");break; case SUNDAY: System.out.println("一觉到下午");break; } } }
经验之谈:
开发中,当需要定义一组常量时,强烈建议使用枚举类。
2.4 enum中常用方法
String toString(): 默认返回的是常量名(对象名),可以继续手动重写该方法! static 枚举类型[] values():返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值,是一个静态方法 static 枚举类型 valueOf(String name):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException。 String name():得到当前枚举常量的名称。建议优先使用toString()。 int ordinal():返回当前枚举常量的次序号,默认从0开始
举例:
import java.util.Scanner; public class TestEnumMethod { public static void main(String[] args) { //values() Week[] values = Week.values(); for (int i = 0; i < values.length; i++) { //ordinal()、name() System.out.println((values[i].ordinal()+1) + "->" + values[i].name()); } System.out.println("------------------------"); Scanner input = new Scanner(System.in); System.out.print("请输入星期值:"); int weekValue = input.nextInt(); Week week = values[weekValue-1]; //toString() System.out.println(week); System.out.print("请输入星期名:"); String weekName = input.next(); //valueOf() week = Week.valueOf(weekName); System.out.println(week); input.close(); } }
2.5 实现接口的枚举类
-
和普通 Java 类一样,枚举类可以实现一个或多个接口
-
若每个枚举值在调用实现的接口方法呈现相同的行为方式,则只要统一实现该方法即可。
-
若需要每个枚举值在调用实现的接口方法呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法
语法:
//1、枚举类可以像普通的类一样,实现接口,并且可以多个,但要求必须实现里面所有的抽象方法! enum A implements 接口1,接口2{ //抽象方法的实现 } //2、如果枚举类的常量可以继续重写抽象方法! enum A implements 接口1,接口2{ 常量名1(参数){ //抽象方法的实现或重写 }, 常量名2(参数){ //抽象方法的实现或重写 }, //... }
举例:
interface Info{ void show(); } //使用enum关键字定义枚举类 enum Season1 implements Info{ //1. 创建枚举类中的对象,声明在enum枚举类的首位 SPRING("春天","春暖花开"){ public void show(){ System.out.println("春天在哪里?"); } }, SUMMER("夏天","夏日炎炎"){ public void show(){ System.out.println("宁静的夏天"); } }, AUTUMN("秋天","秋高气爽"){ public void show(){ System.out.println("秋天是用来分手的季节"); } }, WINTER("冬天","白雪皑皑"){ public void show(){ System.out.println("2002年的第一场雪"); } }; //2. 声明每个对象拥有的属性:private final修饰 private final String SEASON_NAME; private final String SEASON_DESC; //3. 私有化类的构造器 private Season1(String seasonName,String seasonDesc){ this.SEASON_NAME = seasonName; this.SEASON_DESC = seasonDesc; } public String getSEASON_NAME() { return SEASON_NAME; } public String getSEASON_DESC() { return SEASON_DESC; } }
3 泛型
3.1 泛型概述
3.1.1 生活中的例子
-
举例1:中药店,每个抽屉外面贴着标签
-
举例2:超市购物架上很多瓶子,每个瓶子装的是什么,有标签
-
举例3:家庭厨房中:
Java中的泛型,就类似于上述场景中的
标签
。
3.1.2 泛型的引入
在Java中,我们在声明方法时,当在完成方法功能时如果有未知的数据
需要参与,这些未知的数据需要在调用方法时才能确定,那么我们把这样的数据通过形参
表示。在方法体中,用这个形参名来代表那个未知的数据,而调用者在调用时,对应的传入实参
就可以了。
受以上启发,JDK1.5设计了泛型的概念。泛型即为“类型参数
”,这个类型参数在声明它的类、接口或方法中,代表未知的某种通用类型。
举例1:
集合类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在JDK5.0之前只能把元素类型设计为Object,JDK5.0时Java引入了“参数化类型(Parameterized type)”的概念,允许我们在创建集合时指定集合元素的类型。比如:List<String>
,这表明该List只能保存字符串类型的对象。
使用集合存储数据时,除了元素的类型不确定,其他部分是确定的(例如关于这个元素如何保存,如何管理等)。
举例2:
java.lang.Comparable
接口和java.util.Comparator
接口,是用于比较对象大小的接口。这两个接口只是限定了当一个对象大于另一个对象时返回正整数,小于返回负整数,等于返回0,但是并不确定是什么类型的对象比较大小。JDK5.0之前只能用Object类型表示,使用时既麻烦又不安全,因此 JDK5.0 给它们增加了泛型。
其中<T>
就是类型参数,即泛型。
所谓泛型,就是允许在定义类、接口时通过一个
标识
表示类中某个属性的类型
或者是某个方法的返回值或参数的类型
。这个类型参数将在使用时(例如,继承或实现这个接口、创建对象或调用方法时)确定(即传入实际的类型参数,也称为类型实参)。
3.2 使用泛型举例
自从JDK5.0引入泛型的概念之后,对之前核心类库中的API做了很大的修改,例如:JDK5.0改写了集合框架中的全部接口和类、java.lang.Comparable接口、java.util.Comparator接口、Class类等。为这些接口、类增加了泛型支持,从而可以在声明变量、创建对象时传入类型实参。
3.2.1 集合中使用泛型
3.2.1.1 举例
集合中没有使用泛型时:
集合中使用泛型时:
Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。即,把不安全的因素在编译期间就排除了,而不是运行期;既然通过了编译,那么类型一定是符合要求的,就避免了类型转换。
同时,代码更加简洁、健壮。
把一个集合中的内容限制为一个特定的数据类型,这就是generic背后的核心思想。
举例:
//泛型在List中的使用 @Test public void test1(){ //举例:将学生成绩保存在ArrayList中 //标准写法: //ArrayList<Integer> list = new ArrayList<Integer>(); //jdk7的新特性:类型推断 ArrayList<Integer> list = new ArrayList<>(); list.add(56); //自动装箱 list.add(76); list.add(88); list.add(89); //当添加非Integer类型数据时,编译不通过 //list.add("Tom");//编译报错 Iterator<Integer> iterator = list.iterator(); while(iterator.hasNext()){ //不需要强转,直接可以获取添加时的元素的数据类型 Integer score = iterator.next(); System.out.println(score); } }
举例:
//泛型在Map中的使用 @Test public void test2(){ HashMap<String,Integer> map = new HashMap<>(); map.put("Tom",67); map.put("Jim",56); map.put("Rose",88); //编译不通过 // map.put(67,"Jack"); //遍历key集 Set<String> keySet = map.keySet(); for(String str:keySet){ System.out.println(str); } //遍历value集 Collection<Integer> values = map.values(); Iterator<Integer> iterator = values.iterator(); while(iterator.hasNext()){ Integer value = iterator.next(); System.out.println(value); } //遍历entry集 Set<Map.Entry<String, Integer>> entrySet = map.entrySet(); Iterator<Map.Entry<String, Integer>> iterator1 = entrySet.iterator(); while(iterator1.hasNext()){ Map.Entry<String, Integer> entry = iterator1.next(); String key = entry.getKey(); Integer value = entry.getValue(); System.out.println(key + ":" + value); } }
3.2.1.2 练习
练习1:
(1)创建一个ArrayList集合对象,并指定泛型为<Integer> (2)添加5个[0,100)以内的整数到集合中 (3)使用foreach遍历输出5个整数 (4)使用集合的removeIf方法删除偶数,为Predicate接口指定泛型<Ineteger> (5)再使用Iterator迭代器输出剩下的元素,为Iterator接口指定泛型<Integer>
import java.util.ArrayList; import java.util.Iterator; import java.util.Random; import java.util.function.Predicate; public class TestNumber { public static void main(String[] args) { ArrayList<Integer> coll = new ArrayList<Integer>(); Random random = new Random(); for (int i = 1; i <= 5 ; i++) { coll.add(random.nextInt(100)); } System.out.println("coll中5个随机数是:"); for (Integer integer : coll) { System.out.println(integer); } //方式1:使用集合的removeIf方法删除偶数 coll.removeIf(new Predicate<Integer>() { @Override public boolean test(Integer integer) { return integer % 2 == 0; } }); //方式2:调用Iterator接口的remove()方法 //Iterator<Integer> iterator1 = coll.iterator(); //while(coll.hasNext()){ // Integer i = coll.next(); // if(i % 2 == 0){ // coll.remove(); // } //} System.out.println("coll中删除偶数后:"); Iterator<Integer> iterator = coll.iterator(); while(iterator.hasNext()){ Integer number = iterator.next(); System.out.println(number); } } }
3.2.2 比较器中使用泛型
3.2.2.1 举例
package com.atguigu.generic; public class Circle{ private double radius; public Circle(double radius) { super(); this.radius = radius; } public double getRadius() { return radius; } public void setRadius(double radius) { this.radius = radius; } @Override public String toString() { return "Circle [radius=" + radius + "]"; } }
使用泛型之前:
import java.util.Comparator; class CircleComparator implements Comparator{ @Override public int compare(Object o1, Object o2) { //强制类型转换 Circle c1 = (Circle) o1; Circle c2 = (Circle) o2; return Double.compare(c1.getRadius(), c2.getRadius()); } } //测试: public class TestNoGeneric { public static void main(String[] args) { CircleComparator com = new CircleComparator(); System.out.println(com.compare(new Circle(1), new Circle(2))); System.out.println(com.compare("圆1", "圆2"));//运行时异常:ClassCastException } }
使用泛型之后:
import java.util.Comparator; class CircleComparator1 implements Comparator<Circle> { @Override public int compare(Circle o1, Circle o2) { //不再需要强制类型转换,代码更简洁 return Double.compare(o1.getRadius(), o2.getRadius()); } } //测试类 public class TestHasGeneric { public static void main(String[] args) { CircleComparator1 com = new CircleComparator1(); System.out.println(com.compare(new Circle(1), new Circle(2))); //System.out.println(com.compare("圆1", "圆2")); //编译错误,因为"圆1", "圆2"不是Circle类型,是String类型,编译器提前报错, //而不是冒着风险在运行时再报错。 } }
3.2.2.2 练习
(1)声明矩形类Rectangle,包含属性长和宽,属性私有化,提供有参构造、get/set方法、重写toString方法,提供求面积和周长的方法。
(2)矩形类Rectangle实现java.lang.Comparable<T>接口,并指定泛型为<Rectangle>,重写int compareTo(T t)方法,按照矩形面积比较大小,面积相等的,按照周长比较大小。
(3)在测试类中,创建Rectangle数组,并创建5个矩形对象
(4)调用Arrays的sort方法,给矩形数组排序,并显示排序前后的结果。
public class Rectangle implements Comparable<Rectangle>{ private double length; private double width; public Rectangle(double length, double width) { this.length = length; this.width = width; } public double getLength() { return length; } public void setLength(double length) { this.length = length; } public double getWidth() { return width; } public void setWidth(double width) { this.width = width; } //获取面积 public double area(){ return length * width; } //获取周长 public double perimeter(){ return 2 * (length + width); } @Override public String toString() { return "Rectangle{" + "length=" + length + ", width=" + width + ",area =" + area() + ",perimeter = " + perimeter() + '}'; } @Override public int compareTo(Rectangle o) { int compare = Double.compare(area(), o.area()); return compare != 0 ? compare : Double.compare(perimeter(),o.perimeter()); } }
package com.atguigu.genericclass.use; import java.util.Arrays; public class TestRectangle { public static void main(String[] args) { Rectangle[] arr = new Rectangle[4]; arr[0] = new Rectangle(6,2); arr[1] = new Rectangle(4,3); arr[2] = new Rectangle(12,1); arr[3] = new Rectangle(5,4); System.out.println("排序之前:"); for (Rectangle rectangle : arr) { System.out.println(rectangle); } Arrays.sort(arr); System.out.println("排序之后:"); for (Rectangle rectangle : arr) { System.out.println(rectangle); } } }
3.2.3 相关使用说明
-
在创建集合对象的时候,可以指明泛型的类型。
具体格式为:List<Integer> list = new ArrayList<Integer>();
-
JDK7.0时,有新特性,可以简写为:
List<Integer> list = new ArrayList<>(); //类型推断
-
泛型,也称为泛型参数,即参数的类型,只能使用引用数据类型进行赋值。(不能使用基本数据类型,可以使用包装类替换)
-
集合声明时,声明泛型参数。在使用集合时,可以具体指明泛型的类型。一旦指明,类或接口内部,凡是使用泛型参数的位置,都指定为具体的参数类型。如果没有指明的话,看做是Object类型。
3.3 自定义泛型结构
3.3.1 泛型的基础说明
1、<类型>这种语法形式就叫泛型。
-
<类型>的形式我们称为类型参数,这里的"类型"习惯上使用T表示,是Type的缩写。即:<T>。
-
<T>:代表未知的数据类型,我们可以指定为<String>,<Integer>,<Circle>等。
-
类比方法的参数的概念,我们把<T>,称为类型形参,将<Circle>称为类型实参,有助于我们理解泛型
-
-
这里的T,可以替换成K,V等任意字母。
2、在哪里可以声明类型变量<T>
-
声明类或接口时,在类名或接口名后面声明泛型类型,我们把这样的类或接口称为
泛型类
或泛型接口
。
【修饰符】 class 类名<类型变量列表> 【extends 父类】 【implements 接口们】{ } 【修饰符】 interface 接口名<类型变量列表> 【implements 接口们】{ } //例如: public class ArrayList<E> public interface Map<K,V>{ .... }
-
声明方法时,在【修饰符】与返回值类型之间声明类型变量,我们把声明了类型变量的方法,称为泛型方法。
[修饰符] <类型变量列表> 返回值类型 方法名([形参列表])[throws 异常列表]{ //... } //例如:java.util.Arrays类中的 public static <T> List<T> asList(T... a){ .... }
3.3.2 自定义泛型类或泛型接口
当我们在类或接口中定义某个成员时,该成员的相关类型是不确定的,而这个类型需要在使用这个类或接口时才可以确定,那么我们可以使用泛型类、泛型接口。
3.3.2.1 说明
① 我们在声明完自定义泛型类以后,可以在类的内部(比如:属性、方法、构造器中)使用类的泛型。
② 我们在创建自定义泛型类的对象时,可以指明泛型参数类型。一旦指明,内部凡是使用类的泛型参数的位置,都具体化为指定的类的泛型类型。
③ 如果在创建自定义泛型类的对象时,没有指明泛型参数类型,那么泛型将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。
-
经验:泛型要使用一路都用。要不用,一路都不要用。
④ 泛型的指定中必须使用引用数据类型。不能使用基本数据类型,此时只能使用包装类替换。
⑤ 除创建泛型类对象外,子类继承泛型类时、实现类实现泛型接口时,也可以确定泛型结构中的泛型参数。
如果我们在给泛型类提供子类时,子类也不确定泛型的类型,则可以继续使用泛型参数。
我们还可以在现有的父类的泛型参数的基础上,新增泛型参数。
3.3.2.2 注意
① 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>
② JDK7.0 开始,泛型的简化操作:ArrayList<Fruit> flist = new ArrayList<>();
③ 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。
④ 不能使用new E[]。但是可以:E[] elements = (E[])new Object[capacity];
参考:ArrayList源码中声明:Object[] elementData,而非泛型参数类型数组。
⑤ 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,但不可以在静态方法中使用类的泛型。
⑥ 异常类不能是带泛型的。
3.3.2.2 举例
举例1:
class Person<T> { // 使用T类型定义变量 private T info; // 使用T类型定义一般方法 public T getInfo() { return info; } public void setInfo(T info) { this.info = info; } // 使用T类型定义构造器 public Person() { } public Person(T info) { this.info = info; } // static的方法中不能声明泛型 //public static void show(T t) { // //} // 不能在try-catch中使用泛型定义 //public void test() { //try { // //} catch (MyException<T> ex) { // //} //} }
举例2:
class Father<T1, T2> { } // 子类不保留父类的泛型 // 1)没有类型 擦除 class Son1 extends Father {// 等价于class Son extends Father<Object,Object>{ } // 2)具体类型 class Son2 extends Father<Integer, String> { } // 子类保留父类的泛型 // 1)全部保留 class Son3<T1, T2> extends Father<T1, T2> { } // 2)部分保留 class Son4<T2> extends Father<Integer, T2> { }
举例3:
class Father<T1, T2> { } // 子类不保留父类的泛型 // 1)没有类型 擦除 class Son<A, B> extends Father{//等价于class Son extends Father<Object,Object>{ } // 2)具体类型 class Son2<A, B> extends Father<Integer, String> { } // 子类保留父类的泛型 // 1)全部保留 class Son3<T1, T2, A, B> extends Father<T1, T2> { } // 2)部分保留 class Son4<T2, A, B> extends Father<Integer, T2> { }
3.2.3 练习
练习1:
声明一个学生类,该学生包含姓名、成绩,而此时学生的成绩类型不确定,为什么呢,因为,语文老师希望成绩是“优秀”、“良好”、“及格”、“不及格”,数学老师希望成绩是89.5, 65.0,英语老师希望成绩是'A','B','C','D','E'。那么我们在设计这个学生类时,就可以使用泛型。
class Student<T>{ private String name; private T score; public Student() { super(); } public Student(String name, T score) { super(); this.name = name; this.score = score; } public String getName() { return name; } public void setName(String name) { this.name = name; } public T getScore() { return score; } public void setScore(T score) { this.score = score; } @Override public String toString() { return "姓名:" + name + ", 成绩:" + score; } } public class TestStudent { public static void main(String[] args) { //语文老师使用时: Student<String> stu1 = new Student<String>("张三", "良好"); //数学老师使用时: //Student<double> stu2 = new Student<double>("张三", 90.5);//错误,必须是引用数据类型 Student<Double> stu2 = new Student<Double>("张三", 90.5); //英语老师使用时: Student<Character> stu3 = new Student<Character>("张三", 'C'); //错误的指定 //Student<Object> stu = new Student<String>();//错误的 } }
练习2:
定义个泛型类 DAO<T>,在其中定义一个Map 成员变量,Map 的键为 String 类型,值为 T 类型。 分别创建以下方法: public void save(String id,T entity): 保存 T 类型的对象到 Map 成员变量中 public T get(String id):从 map 中获取 id 对应的对象 public void update(String id,T entity):替换 map 中key为id的内容,改为 entity 对象 public List<T> list():返回 map 中存放的所有 T 对象 public void delete(String id):删除指定 id 对象 定义一个 User 类: 该类包含:private成员变量(int类型) id,age;(String 类型)name。 定义一个测试类: 创建 DAO 类的对象, 分别调用其 save、get、update、list、delete 方法来操作 User 对象, 使用 Junit 单元测试类进行测试。
代码实现:
public class DAO<T> { private Map<String,T> map ; { map = new HashMap<String,T>(); } //保存 T 类型的对象到 Map 成员变量中 public void save(String id,T entity){ if(!map.containsKey(id)){ map.put(id,entity); } } //从 map 中获取 id 对应的对象 public T get(String id){ return map.get(id); } //替换 map 中key为id的内容,改为 entity 对象 public void update(String id,T entity){ if(map.containsKey(id)){ map.put(id,entity); } } //返回 map 中存放的所有 T 对象 public List<T> list(){ //错误的: // Collection<T> values = map.values(); // System.out.println(values.getClass()); // return (List<T>) values; //正确的方式1: // ArrayList<T> list = new ArrayList<>(); // Collection<T> values = map.values(); // list.addAll(values); // return list; //正确的方式2: Collection<T> values = map.values(); ArrayList<T> list = new ArrayList<>(values); return list; } //删除指定 id 对象 public void delete(String id){ map.remove(id); } }
import java.util.Objects; public class User { private int id; private int age; private String name; public User() { } public User(int id, int age, String name) { this.id = id; this.age = age; this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "User{" + "id=" + id + ", age=" + age + ", name='" + name + '\'' + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return id == user.id && age == user.age && Objects.equals(name, user.name); } @Override public int hashCode() { return Objects.hash(id, age, name); } }
import java.util.List; public class DAOTest { public static void main(String[] args) { DAO<User> dao = new DAO<>(); dao.save("1001",new User(1,34,"曹操")); dao.save("1002",new User(2,33,"刘备")); dao.save("1003",new User(3,24,"孙权")); dao.update("1002",new User(2,23,"刘禅")); dao.delete("1003"); List<User> list = dao.list(); for(User u : list){ System.out.println(u); } } }
3.3 自定义泛型方法
如果我们定义类、接口时没有使用<泛型参数>,但是某个方法形参类型不确定时,这个方法可以单独定义<泛型参数>。
3.3.1 说明
-
泛型方法的格式:
[访问权限] <泛型> 返回值类型 方法名([泛型标识 参数名称]) [抛出的异常]{ }
-
方法,也可以被泛型化,与其所在的类是否是泛型类没有关系。
-
泛型方法中的泛型参数在方法被调用时确定。
-
泛型方法可以根据需要,声明为static的。
3.3.2 举例
举例1:
public class DAO { public <E> E get(int id, E e) { E result = null; return result; } }
举例2:
public static <T> void fromArrayToCollection(T[] a, Collection<T> c) { for (T o : a) { c.add(o); } } public static void main(String[] args) { Object[] ao = new Object[100]; Collection<Object> co = new ArrayList<Object>(); fromArrayToCollection(ao, co); String[] sa = new String[20]; Collection<String> cs = new ArrayList<>(); fromArrayToCollection(sa, cs); Collection<Double> cd = new ArrayList<>(); // 下面代码中T是Double类,但sa是String类型,编译错误。 // fromArrayToCollection(sa, cd); // 下面代码中T是Object类型,sa是String类型,可以赋值成功。 fromArrayToCollection(sa, co); }
举例3:
class MyArrays { public static <T> void sort(T[] arr){ for (int i = 1; i < arr.length; i++) { for (int j = 0; j < arr.length-i; j++) { if(((Comparable<T>)arr[j]).compareTo(arr[j+1])>0){ T temp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = temp; } } } } } public class MyArraysTest { public static void main(String[] args) { int[] arr = {3,2,5,1,4}; // MyArrays.sort(arr);//错误的,因为int[]不是对象数组 String[] strings = {"hello","java","song"}; MyArrays.sort(strings); System.out.println(Arrays.toString(strings)); Circle[] circles = {new Circle(2.0),new Circle(1.2),new Circle(3.0)}; MyArrays.sort(circles); //编译通过,运行报错,因为Circle没有实现Comparable接口 } }
3.3.3 练习
练习1: 泛型方法
编写一个泛型方法,实现任意引用类型数组指定位置元素交换。
public static <E> void method1( E[] e,int a,int b)
public class Exer01 { //编写一个泛型方法,实现任意引用类型数组指定位置元素交换。 public static <E> void method( E[] arr,int a,int b){ E temp = arr[a]; arr[a] = arr[b]; arr[b] = temp; } @Test public void testMethod(){ Integer[] arr = new Integer[]{10,20,30,40}; method(arr,2,3); for(Integer i : arr){ System.out.println(i); } } }
练习2: 泛型方法
编写一个泛型方法,接收一个任意引用类型的数组,并反转数组中的所有元素
public static <E> void method2( E[] e)
public class Exer01 { //编写一个泛型方法,接收一个任意引用类型的数组,并反转数组中的所有元素 public static <E> void method1( E[] arr){ for(int min = 0,max = arr.length - 1;min < max; min++,max--){ E temp = arr[min]; arr[min] = arr[max]; arr[max] = temp; } } @Test public void testMethod1(){ Integer[] arr = new Integer[]{10,20,30,40}; method1(arr); for(Integer i : arr){ System.out.println(i); } } }
3.4 泛型在继承上的体现
如果B是A的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G<B>并不是G<A>的子类型!
比如:String是Object的子类,但是List<String>并不是List<Object>的子类。
public void testGenericAndSubClass() { Person[] persons = null; Man[] mans = null; //Person[] 是 Man[] 的父类 persons = mans; Person p = mans[0]; // 在泛型的集合上 List<Person> personList = null; List<Man> manList = null; //personList = manList;(报错) }
思考:对比如下两段代码有何不同:
片段1:
public void printCollection(Collection c) { Iterator i = c.iterator(); for (int k = 0; k < c.size(); k++) { System.out.println(i.next()); } }
片段2:
public void printCollection(Collection<Object> c) { for (Object e : c) { System.out.println(e); } }
3.5 通配符的使用
当我们声明一个变量/形参时,这个变量/形参的类型是一个泛型类或泛型接口,例如:Comparator<T>类型,但是我们仍然无法确定这个泛型类或泛型接口的类型变量<T>的具体类型,此时我们考虑使用类型通配符 ? 。
3.5.1 通配符的理解
使用类型通配符:?
比如:List<?>
,Map<?,?>
List<?>
是List<String>
、List<Object>
等各种泛型List的父类。
3.5.2 通配符的读与写
写操作:
将任意元素加入到其中不是类型安全的:
Collection<?> c = new ArrayList<String>(); c.add(new Object()); // 编译时错误
因为我们不知道c的元素类型,我们不能向其中添加对象。add方法有类型参数E作为集合的元素类型。我们传给add的任何参数都必须是一个未知类型的子类。因为我们不知道那是什么类型,所以我们无法传任何东西进去。
唯一可以插入的元素是null,因为它是所有引用类型的默认值。
读操作:
另一方面,读取List<?>的对象list中的元素时,永远是安全的,因为不管 list 的真实类型是什么,它包含的都是Object。
举例1:
public class TestWildcard { public static void m4(Collection<?> coll){ for (Object o : coll) { System.out.println(o); } } }
举例2:
public static void main(String[] args) { List<?> list = null; list = new ArrayList<String>(); list = new ArrayList<Double>(); // list.add(3);//编译不通过 list.add(null); List<String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); l1.add("尚硅谷"); l2.add(15); read(l1); read(l2); } public static void read(List<?> list) { for (Object o : list) { System.out.println(o); } }
3.5.3 使用注意点
注意点1:编译错误:不能用在泛型方法声明上,返回值类型前面<>不能使用?
public static <?> void test(ArrayList<?> list){ }
注意点2:编译错误:不能用在泛型类的声明上
class GenericTypeClass<?>{ }
注意点3:编译错误:不能用在创建对象上,右边属于创建集合对象
ArrayList<?> list2 = new ArrayList<?>();
3.5.4 有限制的通配符
-
<?>
-
允许所有泛型的引用调用
-
-
通配符指定上限:
<? extends 类/接口 >
-
使用时指定的类型必须是继承某个类,或者实现某个接口,即<=
-
-
通配符指定下限:
<? super 类/接口 >
-
使用时指定的类型必须是操作的类或接口,或者是操作的类的父类或接口的父接口,即>=
-
-
说明:
<? extends Number> //(无穷小 , Number] //只允许泛型为Number及Number子类的引用调用 <? super Number> //[Number , 无穷大) //只允许泛型为Number及Number父类的引用调用 <? extends Comparable> //只允许泛型为实现Comparable接口的实现类的引用调用
-
举例1
class Creature{} class Person extends Creature{} class Man extends Person{} class PersonTest { public static <T extends Person> void test(T t){ System.out.println(t); } public static void main(String[] args) { test(new Person()); test(new Man()); //The method test(T) in the type PersonTest is not //applicable for the arguments (Creature) test(new Creature()); } }
-
举例2:
public static void main(String[] args) { Collection<Integer> list1 = new ArrayList<Integer>(); Collection<String> list2 = new ArrayList<String>(); Collection<Number> list3 = new ArrayList<Number>(); Collection<Object> list4 = new ArrayList<Object>(); getElement1(list1); getElement1(list2);//报错 getElement1(list3); getElement1(list4);//报错 getElement2(list1);//报错 getElement2(list2);//报错 getElement2(list3); getElement2(list4); } // 泛型的上限:此时的泛型?,必须是Number类型或者Number类型的子类 public static void getElement1(Collection<? extends Number> coll){} // 泛型的下限:此时的泛型?,必须是Number类型或者Number类型的父类 public static void getElement2(Collection<? super Number> coll){}
-
举例3:
public static void printCollection1(Collection<? extends Person> coll) { //Iterator只能用Iterator<?>或Iterator<? extends Person>.why? Iterator<?> iterator = coll.iterator(); while (iterator.hasNext()) { Person per = iterator.next(); System.out.println(per); } } public static void printCollection2(Collection<? super Person> coll) { //Iterator只能用Iterator<?>或Iterator<? super Person>.why? Iterator<?> iterator = coll.iterator(); while (iterator.hasNext()) { Object obj = iterator.next(); System.out.println(obj); } }
举例4:
@Test public void test1(){ //List<Object> list1 = null; List<Person> list2 = new ArrayList<Person>(); //List<Student> list3 = null; List<? extends Person> list4 = null; list2.add(new Person()); list4 = list2; //读取:可以读 Person p1 = list4.get(0); //写入:除了null之外,不能写入 list4.add(null); // list4.add(new Person()); // list4.add(new Student()); } @Test public void test2(){ //List<Object> list1 = null; List<Person> list2 = new ArrayList<Person>(); //List<Student> list3 = null; List<? super Person> list5 = null; list2.add(new Person()); list5 = list2; //读取:可以实现 Object obj = list5.get(0); //写入:可以写入Person及Person子类的对象 list5.add(new Person()); list5.add(new Student()); }
3.5.5 泛型应用举例
举例1:泛型嵌套
public static void main(String[] args) { HashMap<String, ArrayList<Citizen>> map = new HashMap<String, ArrayList<Citizen>>(); ArrayList<Citizen> list = new ArrayList<Citizen>(); list.add(new Citizen("赵又廷")); list.add(new Citizen("高圆圆")); list.add(new Citizen("瑞亚")); map.put("赵又廷", list); Set<Entry<String, ArrayList<Citizen>>> entrySet = map.entrySet(); Iterator<Entry<String, ArrayList<Citizen>>> iterator = entrySet.iterator(); while (iterator.hasNext()) { Entry<String, ArrayList<Citizen>> entry = iterator.next(); String key = entry.getKey(); ArrayList<Citizen> value = entry.getValue(); System.out.println("户主:" + key); System.out.println("家庭成员:" + value); } }
举例2:个人信息设计
用户在设计类的时候往往会使用类的关联关系,例如,一个人中可以定义一个信息的属性,但是一个人可能有各种各样的信息(如联系方式、基本信息等),所以此信息属性的类型就可以通过泛型进行声明,然后只要设计相应的信息类即可。
interface Info{ // 只有此接口的子类才是表示人的信息 } class Contact implements Info{ // 表示联系方式 private String address ; // 联系地址 private String telephone ; // 联系方式 private String zipcode ; // 邮政编码 public Contact(String address,String telephone,String zipcode){ this.address = address; this.telephone = telephone; this.zipcode = zipcode; } public void setAddress(String address){ this.address = address ; } public void setTelephone(String telephone){ this.telephone = telephone ; } public void setZipcode(String zipcode){ this.zipcode = zipcode; } public String getAddress(){ return this.address ; } public String getTelephone(){ return this.telephone ; } public String getZipcode(){ return this.zipcode; } @Override public String toString() { return "Contact [address=" + address + ", telephone=" + telephone + ", zipcode=" + zipcode + "]"; } } class Introduction implements Info{ private String name ; // 姓名 private String sex ; // 性别 private int age ; // 年龄 public Introduction(String name,String sex,int age){ this.name = name; this.sex = sex; this.age = age; } public void setName(String name){ this.name = name ; } public void setSex(String sex){ this.sex = sex ; } public void setAge(int age){ this.age = age ; } public String getName(){ return this.name ; } public String getSex(){ return this.sex ; } public int getAge(){ return this.age ; } @Override public String toString() { return "Introduction [name=" + name + ", sex=" + sex + ", age=" + age + "]"; } } class Person<T extends Info>{ private T info ; public Person(T info){ // 通过构造器设置信息属性内容 this.info = info; } public void setInfo(T info){ this.info = info ; } public T getInfo(){ return info ; } @Override public String toString() { return "Person [info=" + info + "]"; } } public class GenericPerson{ public static void main(String args[]){ Person<Contact> per = null ; // 声明Person对象 per = new Person<Contact>(new Contact("北京市","01088888888","102206")) ; System.out.println(per); Person<Introduction> per2 = null ; // 声明Person对象 per2 = new Person<Introduction>(new Introduction("李雷","男",24)); System.out.println(per2) ; } }
4 注解(Annotation)
4.1 注解概述
4.1.1 什么是注解
注解(Annotation)是从JDK5.0
开始引入,以“@注解名
”在代码中存在。例如:
@Override
@Deprecated
@SuppressWarnings(value=”unchecked”)
Annotation 可以像修饰符一样被使用,可用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明。还可以添加一些参数值,这些信息被保存在 Annotation 的 “name=value” 对中。
注解可以在类编译、运行时进行加载,体现不同的功能。
4.1.2 注解与注释
注解也可以看做是一种注释,通过使用 Annotation,程序员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。但是,注解,不同于单行注释和多行注释。
-
对于单行注释和多行注释是给程序员看的。
-
而注解是可以被编译器或其他程序读取的。程序还可以根据注解的不同,做出相应的处理。
4.1.3 注解的重要性
在JavaSE中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在JavaEE/Android中注解占据了更重要的角色
,例如用来配置应用程序的任何切面,代替JavaEE旧版中所遗留的繁冗代码
和XML配置
等。
未来的开发模式都是基于注解的,JPA是基于注解的,Spring2.5以上都是基于注解的,Hibernate3.x以后也是基于注解的,Struts2有一部分也是基于注解的了。注解是一种趋势
,一定程度上可以说:框架 = 注解 + 反射 + 设计模式
。
4.2 常见的Annotation作用
示例1:生成文档相关的注解
@author 标明开发该类模块的作者,多个作者之间使用,分割 @version 标明该类模块的版本 @see 参考转向,也就是相关主题 @since 从哪个版本开始增加的 @param 对方法中某参数的说明,如果没有参数就不能写 @return 对方法返回值的说明,如果方法的返回值类型是void就不能写 @exception 对方法可能抛出的异常进行说明 ,如果方法没有用throws显式抛出的异常就不能写
public class JavadocTest { /** * 程序的主方法,程序的入口 * @param args String[] 命令行参数 */ public static void main(String[] args) { } /** * 求圆面积的方法 * @param radius double 半径值 * @return double 圆的面积 */ public static double getArea(double radius){ return Math.PI * radius * radius; } }
示例2:在编译时进行格式检查(JDK内置的三个基本注解)
@Override
: 限定重写父类方法,该注解只能用于方法
@Deprecated
: 用于表示所修饰的元素(类,方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择
@SuppressWarnings
: 抑制编译器警告
public class AnnotationTest{ public static void main(String[] args) { @SuppressWarnings("unused") int a = 10; } @Deprecated public void print(){ System.out.println("过时的方法"); } @Override public String toString() { return "重写的toString方法()"; } }
示例3:跟踪代码依赖性,实现替代配置文件功能
-
Servlet3.0提供了注解(annotation),使得不再需要在web.xml文件中进行Servlet的部署。
@WebServlet("/login") public class LoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) { } protected void doPost(HttpServletRequest request, HttpServletResponse response) { doGet(request, response); } }
<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>
-
Spring框架中关于“事务”的管理
@Transactional(propagation=Propagation.REQUIRES_NEW,isolation=Isolation.READ_COMMITTED,readOnly=false,timeout=3) public void buyBook(String username, String isbn) { //1.查询书的单价 int price = bookShopDao.findBookPriceByIsbn(isbn); //2. 更新库存 bookShopDao.updateBookStock(isbn); //3. 更新用户的余额 bookShopDao.updateUserAccount(username, price); }
<!-- 配置事务属性 --> <tx:advice transaction-manager="dataSourceTransactionManager" id="txAdvice"> <tx:attributes> <!-- 配置每个方法使用的事务属性 --> <tx:method name="buyBook" propagation="REQUIRES_NEW" isolation="READ_COMMITTED" read-only="false" timeout="3" /> </tx:attributes> </tx:advice>
4.3 三个最基本的注解
4.3.1 @Override
-
用于检测被标记的方法为有效的重写方法,如果不是,则报编译错误!
-
只能标记在方法上。
-
它会被编译器程序读取。
4.3.2 @Deprecated
-
用于表示被标记的数据已经过时,不推荐使用。
-
可以用于修饰 属性、方法、构造、类、包、局部变量、参数。
-
它会被编译器程序读取。
4.3.3 @SuppressWarnings
-
抑制编译警告。当我们不希望看到警告信息的时候,可以使用 SuppressWarnings 注解来抑制警告信息
-
可以用于修饰类、属性、方法、构造、局部变量、参数
-
它会被编译器程序读取。
-
可以指定的警告类型有(了解)
-
all,抑制所有警告
-
unchecked,抑制与未检查的作业相关的警告
-
unused,抑制与未用的程式码及停用的程式码相关的警告
-
deprecation,抑制与淘汰的相关警告
-
nls,抑制与非 nls 字串文字相关的警告
-
null,抑制与空值分析相关的警告
-
rawtypes,抑制与使用 raw 类型相关的警告
-
static-access,抑制与静态存取不正确相关的警告
-
static-method,抑制与可能宣告为 static 的方法相关的警告
-
super,抑制与置换方法相关但不含 super 呼叫的警告
-
...
-
示例代码:
import java.util.ArrayList; public class TestAnnotation { @SuppressWarnings("all") public static void main(String[] args) { int i; ArrayList list = new ArrayList(); list.add("hello"); list.add(123); list.add("world"); Father f = new Son(); f.show(); f.methodOl(); } } class Father{ @Deprecated void show() { System.out.println("Father.show"); } void methodOl() { System.out.println("Father Method"); } } class Son extends Father{ /* @Override void method01() { System.out.println("Son Method"); }*/ }
4.4 元注解
JDK1.5在java.lang.annotation包定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。
(1)@Target:用于描述注解的使用范围
-
可以通过枚举类型ElementType的10个常量对象来指定
-
TYPE,METHOD,CONSTRUCTOR,PACKAGE.....
(2)@Retention:用于描述注解的生命周期
-
可以通过枚举类型RetentionPolicy的3个常量对象来指定
-
SOURCE(源代码)、CLASS(字节码)、RUNTIME(运行时)
-
唯有RUNTIME阶段才能被反射读取到
。
(3)@Documented:表明这个注解应该被 javadoc工具记录。
(4)@Inherited:允许子类继承父类中的注解
示例代码:
package java.lang; import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
package java.lang; import java.lang.annotation.*; import static java.lang.annotation.ElementType.*; @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) @Retention(RetentionPolicy.SOURCE) public @interface SuppressWarnings { String[] value(); }
package java.lang; import java.lang.annotation.*; import static java.lang.annotation.ElementType.*; @Documented @Retention(RetentionPolicy.RUNTIME) @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) public @interface Deprecated { }
拓展:元数据
String name = "Tom";
4.5 自定义注解的使用
一个完整的注解应该包含三个部分: (1)声明 (2)使用 (3)读取
4.5.1 声明自定义注解
【元注解】 【修饰符】 @interface 注解名{ 【成员列表】 }
-
自定义注解可以通过四个元注解@Retention,@Target,@Inherited,@Documented,分别说明它的声明周期,使用位置,是否被继承,是否被生成到API文档中。
-
Annotation 的成员在 Annotation 定义中以无参数有返回值的抽象方法的形式来声明,我们又称为配置参数。返回值类型只能是八种基本数据类型、String类型、Class类型、enum类型、Annotation类型、以上所有类型的数组
-
可以使用 default 关键字为抽象方法指定默认返回值
-
如果定义的注解含有抽象方法,那么使用时必须指定返回值,除非它有默认值。格式是“方法名 = 返回值”,如果只有一个抽象方法需要赋值,且方法名为value,可以省略“value=”,所以如果注解只有一个抽象方法成员,建议使用方法名value。
import java.lang.annotation.*; @Inherited @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Table { String value(); }
import java.lang.annotation.*; @Inherited @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Column { String columnName(); String columnType(); }
4.5.2 使用自定义注解
@Table("t_stu") public class Student { @Column(columnName = "sid",columnType = "int") private int id; @Column(columnName = "sname",columnType = "varchar(20)") private String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + '}'; } }
4.5.3 读取和处理自定义注解
自定义注解必须配上注解的信息处理流程才有意义。
我们自己定义的注解,只能使用反射的代码读取。所以自定义注解的声明周期必须是RetentionPolicy.RUNTIME。
4.6 JUnit单元测试
4.6.1 测试分类
黑盒测试:不需要写代码,给输入值,看程序是否能够输出期望的值。
白盒测试:需要写代码的。
4.6.2 JUnit单元测试介绍
JUnit 是由 Erich Gamma 和 Kent Beck 编写的一个测试框架(regression testing framework),供Java开发人员编写单元测试之用。
JUnit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。
要使用JUnit,必须在项目的编译路径中引入JUnit的库
,即相关的.class文件组成的jar包。jar就是一个压缩包,压缩包都是开发好的第三方(Oracle公司第一方,我们自己第二方,其他都是第三方)工具类,都是以class文件形式存在的。
4.6.3 引入本地JUnit.jar
第1步:在项目中File-Project Structure中操作:添加Libraries库
其中,junit-libs包内容如下:
第2步:选择要在哪些module中应用JUnit库
第3步:检查是否应用成功
注意Scope:选择Compile,否则编译时,无法使用JUnit。
第4步:下次如果有新的模块要使用该libs库,这样操作即可
4.6.4 编写和运行@Test单元测试方法
JUnit4版本,要求@Test标记的方法必须满足如下要求:
-
所在的类必须是public的,非抽象的,包含唯一的无参构造器。
-
@Test标记的方法本身必须是public,非抽象的,非静态的,void无返回值,()无参数的。
import org.junit.Test; public class TestJUnit { @Test public void test01(){ System.out.println("TestJUnit.test01"); } @Test public void test02(){ System.out.println("TestJUnit.test02"); } @Test public void test03(){ System.out.println("TestJUnit.test03"); } }
4.6.5 设置执行JUnit用例时支持控制台输入
1. 设置数据:
默认情况下,在单元测试方法中使用Scanner时,并不能实现控制台数据的输入。需要做如下设置:
在idea64.exe.vmoptions配置文件
中加入下面一行设置,重启idea后生效。
-Deditable.java.test.console=true
2. 配置文件位置:
添加完成之后,重启IDEA即可。
3. 如果上述位置设置不成功,需要继续修改如下位置
修改位置1:IDEA安装目录的bin目录(例如:D:\develop_tools\IDEA\IntelliJ IDEA 2022.1.2\bin
)下的idea64.exe.vmoptions文件。
修改位置2:C盘的用户目录C:\Users\用户名\AppData\Roaming\JetBrains\IntelliJIdea2022.1
下的idea64.exe.vmoptions`件。
5 异常的处理
5.1 异常处理概述
在编写程序时,经常要在可能出现错误的地方加上检测的代码,如进行x/y运算时,要检测分母为0
,数据为空
,输入的不是数据而是字符
等。过多的if-else分支会导致程序的代码加长
、臃肿
,可读性差
,程序员需要花很大的精力“堵漏洞
”。因此采用异常处理机制。
Java异常处理
Java采用的异常处理机制,是将异常处理的程序代码集中在一起
,与正常的程序代码分开,使得程序简洁、优雅,并易于维护。
Java异常处理的方式:
方式一:try-catch-finally
方式二:throws + 异常类型
5.2 方式1:捕获异常(try-catch-finally)
Java提供了异常处理的抓抛模型。
-
前面提到,Java程序的执行过程中如出现异常,会生成一个异常类对象,该异常对象将被提交给Java运行时系统,这个过程称为
抛出(throw)异常
。 -
如果一个方法内抛出异常,该异常对象会被抛给调用者方法中处理。如果异常没有在调用者方法中处理,它继续被抛给这个调用方法的上层方法。这个过程将一直继续下去,直到异常被处理。这一过程称为
捕获(catch)异常
。 -
如果一个异常回到main()方法,并且main()也不处理,则程序运行终止。
5.2.1 try-catch-finally基本格式
捕获异常语法如下:
try{ ...... //可能产生异常的代码 } catch( 异常类型1 e ){ ...... //当产生异常类型1型异常时的处置措施 } catch( 异常类型2 e ){ ...... //当产生异常类型2型异常时的处置措施 } finally{ ...... //无论是否发生异常,都无条件执行的语句 }
1、整体执行过程:
当某段代码可能发生异常,不管这个异常是编译时异常(受检异常)还是运行时异常(非受检异常),我们都可以使用try块将它括起来,并在try块下面编写catch分支尝试捕获对应的异常对象。
-
如果在程序运行时,try块中的代码没有发生异常,那么catch所有的分支都不执行。
-
如果在程序运行时,try块中的代码发生了异常,根据异常对象的类型,将从上到下选择第一个匹配的catch分支执行。此时try中发生异常的语句下面的代码将不执行,而整个try...catch之后的代码可以继续运行。
-
如果在程序运行时,try块中的代码发生了异常,但是所有catch分支都无法匹配(捕获)这个异常,那么JVM将会终止当前方法的执行,并把异常对象“抛”给调用者。如果调用者不处理,程序就挂了。
2、try
-
捕获异常的第一步是用
try{…}语句块
选定捕获异常的范围,将可能出现异常的业务逻辑代码放在try语句块中。
3、catch (Exceptiontype e)
-
catch分支,分为两个部分,catch()中编写异常类型和异常参数名,{}中编写如果发生了这个异常,要做什么处理的代码。
-
如果明确知道产生的是何种异常,可以用该异常类作为catch的参数;也可以用其父类作为catch的参数。
比如:可以用ArithmeticException类作为参数的地方,就可以用RuntimeException类作为参数,或者用所有异常的父类Exception类作为参数。但不能是与ArithmeticException类无关的异常,如NullPointerException(catch中的语句将不会执行)。
-
每个try语句块可以伴随一个或多个catch语句,用于处理可能产生的不同类型的异常对象。
-
如果有多个catch分支,并且多个异常类型有父子类关系,必须保证小的子异常类型在上,大的父异常类型在下。否则,报错。
-
catch中常用异常处理的方式
-
public String getMessage()
:获取异常的描述信息,返回字符串 -
public void printStackTrace()
:打印异常的跟踪栈信息并输出到控制台。包含了异常的类型、异常的原因、还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace()。
-
5.2.2 使用举例
举例1:
public class IndexOutExp { public static void main(String[] args) { String friends[] = { "lisa", "bily", "kessy" }; try { for (int i = 0; i < 5; i++) { System.out.println(friends[i]); } } catch (ArrayIndexOutOfBoundsException e) { System.out.println("index err"); } System.out.println("\nthis is the end"); } }
举例2:
public class DivideZero1 { int x; public static void main(String[] args) { int y; DivideZero1 c = new DivideZero1(); try { y = 3 / c.x; } catch (ArithmeticException e) { System.out.println("divide by zero error!"); } System.out.println("program ends ok!"); } }
举例3:
@Test public void test1(){ try{ String str1 = "atguigu.com"; str1 = null; System.out.println(str1.charAt(0)); }catch(NullPointerException e){ //异常的处理方式1 System.out.println("不好意思,亲~出现了小问题,正在加紧解决..."); }catch(ClassCastException e){ //异常的处理方式2 System.out.println("出现了类型转换的异常"); }catch(RuntimeException e){ //异常的处理方式3 System.out.println("出现了运行时异常"); } //此处的代码,在异常被处理了以后,是可以正常执行的 System.out.println("hello"); }
5.2.3 finally使用及举例
-
因为异常会引发程序跳转,从而会导致有些语句执行不到。而程序中有一些特定的代码无论异常是否发生,都
需要执行
。例如,数据库连接、输入流输出流、Socket连接、Lock锁的关闭等,这样的代码通常就会放到finally块中。所以,我们通常将一定要被执行的代码声明在finally中。-
唯一的例外,使用 System.exit(0) 来终止当前正在运行的 Java 虚拟机。
-
-
不论在try代码块中是否发生了异常事件,catch语句是否执行,catch语句是否有异常,catch语句中是否有return,finally块中的语句都会被执行。
-
finally语句和catch语句是可选的,但finally不能单独使用。
try{ }finally{ }
举例1:确保资源关闭
import java.util.InputMismatchException; import java.util.Scanner; public class TestFinally { public static void main(String[] args) { Scanner input = new Scanner(System.in); try { System.out.print("请输入第一个整数:"); int a = input.nextInt(); System.out.print("请输入第二个整数:"); int b = input.nextInt(); int result = a/b; System.out.println(a + "/" + b +"=" + result); } catch (InputMismatchException e) { System.out.println("数字格式不正确,请输入两个整数"); }catch (ArithmeticException e){ System.out.println("第二个整数不能为0"); } finally { System.out.println("程序结束,释放资源"); input.close(); } } @Test public void test1(){ FileInputStream fis = null; try{ File file = new File("hello1.txt"); fis = new FileInputStream(file);//FileNotFoundException int b = fis.read();//IOException while(b != -1){ System.out.print((char)b); b = fis.read();//IOException } }catch(IOException e){ e.printStackTrace(); }finally{ try { if(fis != null) fis.close();//IOException } catch (IOException e) { e.printStackTrace(); } } } }
举例2:从try回来
public class FinallyTest1 { public static void main(String[] args) { int result = test("12"); System.out.println(result); } public static int test(String str){ try{ Integer.parseInt(str); return 1; }catch(NumberFormatException e){ return -1; }finally{ System.out.println("test结束"); } } }
举例3:从catch回来
public class FinallyTest2 { public static void main(String[] args) { int result = test("a"); System.out.println(result); } public static int test(String str) { try { Integer.parseInt(str); return 1; } catch (NumberFormatException e) { return -1; } finally { System.out.println("test结束"); } } }
举例4:从finally回来
public class FinallyTest3 { public static void main(String[] args) { int result = test("a"); System.out.println(result); } public static int test(String str) { try { Integer.parseInt(str); return 1; } catch (NumberFormatException e) { return -1; } finally { System.out.println("test结束"); return 0; } } }
笔试题:
public class ExceptionTest { public static void main(String[] args) { int result = test(); System.out.println(result); //100 } public static int test(){ int i = 100; try { return i; } finally { i++; } } }
笔试题:final、finally、finalize有什么区别?
5.2.4 异常处理的体会
-
前面使用的异常都是
RuntimeException类
或是它的子类
,这些类的异常的特点是:即使没有使用try和catch捕获,Java自己也能捕获,并且编译通过 ( 但运行时会发生异常使得程序运行终止 )。所以,对于这类异常,可以不作处理,因为这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响。 -
如果抛出的异常是IOException等类型的
非运行时异常
,则必须捕获,否则编译错误
。也就是说,我们必须处理编译时异常,将异常进行捕捉,转化为运行时异常。
5.3 方式2:声明抛出异常类型(throws)
-
如果在编写方法体的代码时,某句代码可能发生某个
编译时异常
,不处理编译不通过,但是在当前方法体中可能不适合处理
或无法给出合理的处理方式
,则此方法应显示地
声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理。 -
具体方式:在方法声明中用
throws语句
可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类。
5.3.1 throws基本格式
声明异常格式:
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{ }
在throws后面可以写多个异常类型,用逗号隔开。
举例:
public void readFile(String file) throws FileNotFoundException,IOException { ... // 读文件的操作可能产生FileNotFoundException或IOException类型的异常 FileInputStream fis = new FileInputStream(file); //... }
5.3.2 throws 使用举例
举例:针对于编译时异常
public class TestThrowsCheckedException { public static void main(String[] args) { System.out.println("上课....."); try { afterClass();//换到这里处理异常 } catch (InterruptedException e) { e.printStackTrace(); System.out.println("准备提前上课"); } System.out.println("上课....."); } public static void afterClass() throws InterruptedException { for(int i=10; i>=1; i--){ Thread.sleep(1000);//本来应该在这里处理异常 System.out.println("距离上课还有:" + i + "分钟"); } } }
举例:针对于运行时异常:
throws后面也可以写运行时异常类型,只是运行时异常类型,写或不写对于编译器和程序执行来说都没有任何区别。如果写了,唯一的区别就是调用者调用该方法后,使用try...catch结构时,IDEA可以获得更多的信息,需要添加哪种catch分支。
import java.util.InputMismatchException; import java.util.Scanner; public class TestThrowsRuntimeException { public static void main(String[] args) { Scanner input = new Scanner(System.in); try { System.out.print("请输入第一个整数:"); int a = input.nextInt(); System.out.print("请输入第二个整数:"); int b = input.nextInt(); int result = divide(a,b); System.out.println(a + "/" + b +"=" + result); } catch (ArithmeticException | InputMismatchException e) { e.printStackTrace(); } finally { input.close(); } } public static int divide(int a, int b)throws ArithmeticException{ return a/b; } }
5.3.3 方法重写中throws的要求
方法重写时,对于方法签名是有严格要求的。复习:
(1)方法名必须相同 (2)形参列表必须相同 (3)返回值类型 - 基本数据类型和void:必须相同 - 引用数据类型:<= (4)权限修饰符:>=,而且要求父类被重写方法在子类中是可见的 (5)不能是static,final修饰的方法
此外,对于throws异常列表要求:
-
如果父类被重写方法的方法签名后面没有 “throws 编译时异常类型”,那么重写方法时,方法签名后面也不能出现“throws 编译时异常类型”。
-
如果父类被重写方法的方法签名后面有 “
throws 编译时异常类型
”,那么重写方法时,throws的编译时异常类型必须 <= 被重写方法throws的编译时异常类型,或者不throws编译时异常。 -
方法重写,对于“
throws 运行时异常类型
”没有要求。
import java.io.IOException; class Father{ public void method()throws Exception{ System.out.println("Father.method"); } } class Son extends Father{ @Override public void method() throws IOException,ClassCastException { System.out.println("Son.method"); } }
5.4 两种异常处理方式的选择
前提:对于异常,使用相应的处理方式。此时的异常,主要指的是编译时异常。
-
如果程序代码中,涉及到资源的调用(流、数据库连接、网络连接等),则必须考虑使用try-catch-finally来处理,保证不出现内存泄漏。
-
如果父类被重写的方法没有throws异常类型,则子类重写的方法中如果出现异常,只能考虑使用try-catch-finally进行处理,不能throws。
-
开发中,方法a中依次调用了方法b,c,d等方法,方法b,c,d之间是递进关系。此时,如果方法b,c,d中有异常,我们通常选择使用throws,而方法a中通常选择使用try-catch-finally。
6 多线程
6.1 创建和启动线程
6.1.1 概述
-
Java语言的JVM允许程序运行多个线程,使用
java.lang.Thread
类代表线程,所有的线程对象都必须是Thread类或其子类的实例。 -
Thread类的特性
-
每个线程都是通过某个特定Thread对象的run()方法来完成操作的,因此把run()方法体称为
线程执行体
。 -
通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
-
要想实现多线程,必须在主线程中创建新的线程对象。
-
6.1.2 方式1:继承Thread类
Java通过继承Thread类来创建并启动多线程的步骤如下:
-
定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务
-
创建Thread子类的实例,即创建了线程对象
-
调用线程对象的start()方法来启动该线程
代码如下:
//自定义线程类 public class MyThread extends Thread { //定义指定线程名称的构造方法 public MyThread(String name) { //调用父类的String参数的构造方法,指定线程的名称 super(name); } /** * 重写run方法,完成该线程执行的逻辑 */ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(getName()+":正在执行!"+i); } } }
测试类:
public class TestMyThread { public static void main(String[] args) { //创建自定义线程对象1 MyThread mt1 = new MyThread("子线程1"); //开启子线程1 mt1.start(); //创建自定义线程对象2 MyThread mt2 = new MyThread("子线程2"); //开启子线程2 mt2.start(); //在主方法中执行for循环 for (int i = 0; i < 10; i++) { System.out.println("main线程!"+i); } } }
注意:
如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
想要启动多线程,必须调用start方法。
一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“
IllegalThreadStateException
”。
6.1.3 方式2:实现Runnable接口
Java有单继承的限制,当我们无法继承Thread类时,那么该如何做呢?在核心类库中提供了Runnable接口,我们可以实现Runnable接口,重写run()方法,然后再通过Thread类的对象代理启动和执行我们的线程体run()方法
步骤如下:
-
定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
-
创建Runnable实现类的实例,并以此实例作为Thread的target参数来创建Thread对象,该Thread对象才是真正 的线程对象。
-
调用线程对象的start()方法,启动线程。调用Runnable接口实现类的run方法。
代码如下:
public class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } }
测试类:
package com.atguigu.thread; public class TestMyRunnable { public static void main(String[] args) { //创建自定义类对象 线程任务对象 MyRunnable mr = new MyRunnable(); //创建线程对象 Thread t = new Thread(mr, "长江"); t.start(); for (int i = 0; i < 20; i++) { System.out.println("黄河 " + i); } } }
通过实现Runnable接口,使得该类有了多线程类的特征。所有的分线程要执行的代码都在run方法里面。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
实际上,所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现 Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
说明:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。 而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。
6.1.4 变形写法
使用匿名内部类对象来实现线程的创建和启动
new Thread("新的线程!"){ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(getName()+":正在执行!"+i); } } }.start();
new Thread(new Runnable(){ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()+":" + i); } } }).start();
6.1.5 对比两种方式
联系
Thread类实际上也是实现了Runnable接口的类。即:
public class Thread extends Object implements Runnable
区别
-
继承Thread:线程代码存放Thread子类run方法中。
-
实现Runnable:线程代码存在接口的子类的run方法。
实现Runnable接口比继承Thread类所具有的优势
-
避免了单继承的局限性
-
多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
-
增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
6.1.6 练习
创建两个分线程,让其中一个线程输出1-100之间的偶数,另一个线程输出1-100之间的奇数。
6.2 Thread类的常用结构
6.2.1 构造器
-
public Thread() :分配一个新的线程对象。
-
public Thread(String name) :分配一个指定名字的新的线程对象。
-
public Thread(Runnable target) :指定创建线程的目标对象,它实现了Runnable接口中的run方法
-
public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
6.2.2 常用方法系列1
-
public void run() :此线程要执行的任务在此处定义代码。
-
public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
-
public String getName() :获取当前线程名称。
-
public void setName(String name):设置该线程名称。
-
public static Thread currentThread() :返回对当前正在执行的线程对象的引用。在Thread子类中就是this,通常用于主线程和Runnable实现类
-
public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
-
public static void yield():yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。
6.2.3 常用方法系列2
-
public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。
-
void join() :等待该线程终止。
void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。
void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
-
public final void stop():
已过时
,不建议使用。强行结束一个线程的执行,直接进入死亡状态。run()即刻停止,可能会导致一些清理性的工作得不到完成,如文件,数据库等的关闭。同时,会立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题。 -
void suspend() / void resume() : 这两个操作就好比播放器的暂停和恢复。二者必须成对出现,否则非常容易发生死锁。suspend()调用会导致线程暂停,但不会释放任何锁资源,导致其它线程都无法访问被它占用的锁,直到调用resume()。
已过时
,不建议使用。
6.2.4 常用方法系列3
每个线程都有一定的优先级,同优先级线程组成先进先出队列(先到先服务),使用分时调度策略。优先级高的线程采用抢占式策略,获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。
-
Thread类的三个优先级常量:
-
MAX_PRIORITY(10):最高优先级
-
MIN _PRIORITY (1):最低优先级
-
NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。
-
-
public final int getPriority() :返回线程优先级
-
public final void setPriority(int newPriority) :改变线程的优先级,范围在[1,10]之间。
练习:获取main线程对象的名称和优先级。
声明一个匿名内部类继承Thread类,重写run方法,在run方法中获取线程名称和优先级。设置该线程优先级为最高优先级并启动该线程。
public static void main(String[] args) { Thread t = new Thread(){ public void run(){ System.out.println(getName() + "的优先级:" + getPriority()); } }; t.setPriority(Thread.MAX_PRIORITY); t.start(); System.out.println(Thread.currentThread().getName() +"的优先级:" + Thread.currentThread().getPriority()); }
案例:
-
声明一个匿名内部类继承Thread类,重写run方法,实现打印[1,100]之间的偶数,要求每隔1秒打印1个偶数。
-
声明一个匿名内部类继承Thread类,重写run方法,实现打印[1,100]之间的奇数,
-
当打印到5时,让奇数线程暂停一下,再继续。
-
当打印到5时,让奇数线程停下来,让偶数线程执行完再打印。
-
public class TestThreadStateChange { public static void main(String[] args) { Thread te = new Thread() { @Override public void run() { for (int i = 2; i <= 100; i += 2) { System.out.println("偶数线程:" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; te.start(); Thread to = new Thread() { @Override public void run() { for (int i = 1; i <= 100; i += 2) { System.out.println("奇数线程:" + i); if (i == 5) { // Thread.yield(); try { te.join(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }; to.start(); } }
6.2.5 守护线程(了解)
有一种线程,它是在后台运行的,它的任务是为其他线程提供服务的,这种线程被称为“守护线程”。JVM的垃圾回收线程就是典型的守护线程。
守护线程有个特点,就是如果所有非守护线程都死亡,那么守护线程自动死亡。形象理解:兔死狗烹
,鸟尽弓藏
调用setDaemon(true)方法可将指定线程设置为守护线程。必须在线程启动之前设置,否则会报IllegalThreadStateException异常。
调用isDaemon()可以判断线程是否是守护线程。
public class TestThread { public static void main(String[] args) { MyDaemon m = new MyDaemon(); m.setDaemon(true); m.start(); for (int i = 1; i <= 100; i++) { System.out.println("main:" + i); } } } class MyDaemon extends Thread { public void run() { while (true) { System.out.println("我一直守护者你..."); try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } }
6.3 多线程的生命周期
Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下一些状态:
6.3.1 JDK1.5之前:5种状态
线程的生命周期有五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。CPU需要在多条线程之间切换,于是线程状态会多次在运行、阻塞、就绪之间切换。
1.新建
当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。此时它和其他Java对象一样,仅仅由JVM为其分配了内存,并初始化了实例变量的值。此时的线程对象并没有任何线程的动态特征,程序也不会执行它的线程体run()。
2.就绪
但是当线程对象调用了start()方法之后,就不一样了,线程就从新建状态转为就绪状态。JVM会为其创建方法调用栈和程序计数器,当然,处于这个状态中的线程并没有开始运行,只是表示已具备了运行的条件,随时可以被调度。至于什么时候被调度,取决于JVM里线程调度器的调度。
注意:
程序只能对新建状态的线程调用start(),并且只能调用一次,如果对非新建状态的线程,如已启动的线程或已死亡的线程调用start()都会报错IllegalThreadStateException异常。
3.运行
如果处于就绪状态的线程获得了CPU资源时,开始执行run()方法的线程体代码,则该线程处于运行状态。如果计算机只有一个CPU核心,在任何时刻只有一个线程处于运行状态,如果计算机有多个核心,将会有多个线程并行(Parallel)执行。
当然,美好的时光总是短暂的,而且CPU讲究雨露均沾。对于抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务,当该时间用完,系统会剥夺该线程所占用的资源,让其回到就绪状态等待下一次被调度。此时其他线程将获得执行机会,而在选择下一个线程时,系统会适当考虑线程的优先级。
4.阻塞
当在运行过程中的线程遇到如下情况时,会让出 CPU 并临时中止自己的执行,进入阻塞状态:
-
线程调用了sleep()方法,主动放弃所占用的CPU资源;
-
线程试图获取一个同步监视器,但该同步监视器正被其他线程持有;
-
线程执行过程中,同步监视器调用了wait(),让它等待某个通知(notify);
-
线程执行过程中,同步监视器调用了wait(time)
-
线程执行过程中,遇到了其他线程对象的加塞(join);
-
线程被调用suspend方法被挂起(已过时,因为容易发生死锁);
当前正在执行的线程被阻塞后,其他线程就有机会执行了。针对如上情况,当发生如下情况时会解除阻塞,让该线程重新进入就绪状态,等待线程调度器再次调度它:
-
线程的sleep()时间到;
-
线程成功获得了同步监视器;
-
线程等到了通知(notify);
-
线程wait的时间到了
-
加塞的线程结束了;
-
被挂起的线程又被调用了resume恢复方法(已过时,因为容易发生死锁);
5.死亡
线程会以以下三种方式之一结束,结束后的线程就处于死亡状态:
-
run()方法执行完成,线程正常结束
-
线程执行过程中抛出了一个未捕获的异常(Exception)或错误(Error)
-
直接调用该线程的stop()来结束该线程(已过时)
6.3.2 JDK1.5及之后:6种状态
在java.lang.Thread.State的枚举类中这样定义:
public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED; }
-
NEW(新建)
:线程刚被创建,但是并未启动。还没调用start方法。 -
RUNNABLE(可运行)
:这里没有区分就绪和运行状态。因为对于Java对象来说,只能标记为可运行,至于什么时候运行,不是JVM来控制的了,是OS来进行调度的,而且时间非常短暂,因此对于Java对象的状态来说,无法区分。 -
Teminated(被终止)
:表明此线程已经结束生命周期,终止运行。 -
重点说明,根据Thread.State的定义,阻塞状态分为三种:
BLOCKED
、WAITING
、TIMED_WAITING
。-
BLOCKED(锁阻塞)
:在API中的介绍为:一个正在阻塞、等待一个监视器锁(锁对象)的线程处于这一状态。只有获得锁对象的线程才能有执行机会。-
比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。
-
-
TIMED_WAITING(计时等待)
:在API中的介绍为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。-
当前线程执行过程中遇到Thread类的
sleep
或join
,Object类的wait
,LockSupport类的park
方法,并且在调用这些方法时,设置了时间
,那么当前线程会进入TIMED_WAITING,直到时间到,或被中断。
-
-
WAITING(无限等待)
:在API中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。-
当前线程执行过程中遇到遇到Object类的
wait
,Thread类的join
,LockSupport类的park
方法,并且在调用这些方法时,没有指定时间
,那么当前线程会进入WAITING状态,直到被唤醒。-
通过Object类的wait进入WAITING状态的要有Object的notify/notifyAll唤醒;
-
通过Condition的await进入WAITING状态的要有Condition的signal方法唤醒;
-
通过LockSupport类的park方法进入WAITING状态的要有LockSupport类的unpark方法唤醒
-
通过Thread类的join进入WAITING状态,只有调用join方法的线程对象结束才能让当前线程恢复;
-
-
-