二、面对对象---5.
(一)接口
176、什么是接口和为什么需要接口
177、接口的定义和接口成员
多个抽象类的抽象就是接口。Java中最小的程序单元就是类,接口其实就是一个特殊的类。Java中的接口表示规范,用于定义一组方法,表示某一类事务必须要具备的功能,要求实现类必须来实现该接口并提供方法实现。
定义类语法: [public] class 类名{}
定义接口语法:[public] interface 接口名{},在这里还没有考虑接口的父接口等等
接口起名问题:表示具有某些能力,有些人习惯以able结尾。Walkable,表示可以行走的;有的公司或个人习惯以I打头,表示接口如:IWalkable.java
成功编译之后和类一样,具有一份字节码。反编译工具2:jd_gui
接口存在的成员:
1):接口中没有构造器,接口不能new(创建对象),接口中不能定义普通方法
2):接口中定义的成员变量实质是全局静态常量(默认使用public static final修饰)。如:public static final String NAME = "龙17"。(有人喜欢用接口来封装多个常量信息称之为常量接口,其目的和常量类相同,不推荐)
(标志接口:接口中没有任何成员,就仅仅是一个接口的定义,就是一个标志。其他类的实现该接口,就属于该家族,我们可以通过第三方代码赋予该接口实现类特殊的功能,不推荐)
3):接口中定义的方法都是公共的抽象方法,默认使用public abstract 来修饰,如:public abstract void walk(); 一般地,我们在接口中定义方法,不喜欢用修饰符
4):接口中定义的内部类都是公共的静态内部类,默认使用public static来修饰内部类,如:public static interface ABC{}
//定义一个接口
interface IWalkable
{
String NAME = "龙17"; //等价于 public static final String NAME = "龙17"
void walk(); //等价于public abstract void walk();
interface ABC{} //等价于 public static interface ABC{}
//IWalkable(){} //错误: 需要<标识符> ,接口中不能定义构造器
void walk(){}; //错误: 已在接口 IWalkable中定义了方法 walk() ,接口中不能定义普通方法
}
//演示接口
public class InterfaceDemo
{
public static void main(String[] args)
{
System.out.println("Hello World!");
}
}
178、接口的特点和接口的继承
接口的特点:
- 没有构造方法,也不能显示定义构造器,不能实例化。
- 接口只能继承接口,不能继承类,且接口支持多继承(类是单继承关系)。 语法:[修饰符] interface 接口名 extends 接口1,接口2
- 接口里的方法全是抽象的,默认修饰符是public abstract。
- 接口里的字段全是全局静态常量,默认修饰符是public static final。
- 接口里的内部类全是静态的,默认修饰符是public static。
实现类和接口之间只有实现关系,没有继承关系,使用implements来表示;
接口和接口之间只能是继承关系,使用extends来表示;
类和类之间也可以有继承关系,使用extends来表示。
//定义一个接口
//爬行动物规范
interface IWalkable
{
void walk();
}
//水生动物规范
interface ISwimable
{
void swim();
}
//两栖动物
interface Amphibiousable extends IWalkable,ISwimable
{
}
179、接口的实现关系
接口的实现者:实现类
接口仅仅只是定义了某一类事物应该具有某些功能,但是没有提供任何实现。
此时,我们得提供类,再让该类去实现接口,并覆盖接口中的方法,从而实现了接口中定义的功能。
类实现接口的方法:[修饰符] class 类名 extends 父类 implements 接口1,接口2{}
一个类可以实现多个接口,从而也弥补了类的单继承问题。
接口和实现类之间的关系,严格上称之为“实现关系”,使用implements来表示,但是在开发,有时候为了方便也把这个实现关系称之为特殊继承关系。所以可以这样理解:接口是实现类的父类。
面向接口编程:
接口 变量 = 创建实现类对象; //体现了多态思想
接口和实现类的多态关系才是我们见得最多的。
因为接口中的方法是抽象的,公共的,所以实现类必须覆盖接口中的方法,并且方法必须使用public修饰。
//定义一个接口
//爬行动物规范
interface IWalkable
{
void walk();
}
//水生动物规范
interface ISwimable
{
void swim();
}
//两栖动物
interface Amphibiousable extends IWalkable,ISwimable
{
}
class Animal
{
}
//猫
class Cat extends Animal implements IWalkable
{
public void walk()
{
System.out.println("走猫步");
}
}
class Demo1
{
public static void main(String[] args)
{
//创建猫对象,并调用方法
Cat c = new Cat(); //唾弃,鄙视
IWalkable w = new Cat(); //面向接口编程,存在多态
w.walk(); //体现多态特征:执行Cat类中的walk方法
}
}
180、类和类以及类和接口的关系图
//定义一个接口
//爬行动物规范
interface IWalkable
{
void walk();
}
//水生动物规范
interface ISwimable
{
void swim();
}
//两栖动物
interface Amphibiousable extends IWalkable,ISwimable
{
}
class Animal
{
}
//猫
class Cat extends Animal implements IWalkable
{
public void walk()
{
System.out.println("走猫步");
}
}
//鱼
class Fish extends Animal implements ISwimable
{
public void swim()
{
System.out.println("游啊游");
}
}
//青蛙,一个类实现多个接口
class Frog extends Animal implements IWalkable,ISwimable
{
public void walk()
{
System.out.println("跳跃");
}
public void swim()
{
System.out.println("蛙泳");
}
}
class Demo1
{
public static void main(String[] args)
{
//创建猫对象,并调用方法2
Cat c = new Cat(); //唾弃,鄙视
IWalkable w = new Cat(); //面向接口编程,存在多态
w.walk(); //体现多态特征:执行Cat类中的walk方法
ISwimable f = new Fish();
f.swim();
Frog frog = new Frog();
frog.swim();
frog.walk();
}
}
181、接口和抽象类的区别
接口和抽象类的区别:
相同点:
- 都位于继承的顶端,用于被其他类实现或继承
- 都不能实例化
- 都可以定义抽象方法,其子类/实现类都必须覆写这些抽象方法
区别:
- 接口没有构造方法,抽象类有构造方法
- 抽象类可包含普通方法和抽象方法,接口只能包含抽象方法(java8之前)
- 一个类只能继承一个直接父类(可能是抽象类),接口是多继承的并且只支持一个类实现多个接口(接口弥补了java的单继承)
- 成员变量:接口里默认是public static final,抽象类是默认包权限
- 方法:接口里默认是public abstract,抽象类默认是默认包访问权限
- 内部类:接口里默认是public static,抽象类默认是默认包访问权限
-------------------------
如果接口和实现类可以完成相同的功能,尽量使用接口,面向接口编程
设计模式:接口和抽象类结合使用的(适配器模式)
182、面向接口编程思想
面向接口编程:多态的好处:把实现类对象赋给接口类型变量,屏蔽了不同实现类之间的实现差异,从而可以做到通用编程
案例:使用USB设备来工作
//指定USB规范
interface IUSB
{
public void swapData();
}
//USB鼠标
class Mouse implements IUSB
{
public void swapData()
{
System.out.println("鼠标在移动");
}
}
//USB打印机
class Print implements IUSB
{
public void swapData()
{
System.out.println("打印");
}
}
//主板
class MotherBoard
{
private static IUSB[] usbs = new IUSB[6];
private static int index = 0;
//把设备插入到主板中的功能,接收IUSB类型的对象
public static void pluginIn(IUSB usb)
{
if (index == usbs.length)
{
System.out.println("USB插槽已经插满了");
return;
}
usbs[index] = usb;
index++;
}
//取出主板中的每一个USB设备,并工作
public static void doWork()
{
for (IUSB usb : usbs )
{
if (usb != null)
{
usb.swapData();
}
}
}
}
//面向接口编程 案例:使用USB设备来工作
class USBDemo
{
public static void main(String[] args)
{
//创建鼠标对象
MotherBoard.pluginIn(new Mouse());
//安装打印机
MotherBoard.pluginIn(new Print());
//调用主板的工作方法
MotherBoard.doWork();
}
}
(二)内部类
183、内部类概述
内部类:定义在类结构中的另一个类
类中定义的成员:字段、方法、内部类
为什么使用内部类:
- 增强封装,把内部类隐藏在外部类之内,不许其他类访问该类
- 内部类能提高代码的可读性和可维护性,把小型类嵌入到外部类中结构上代码更靠近
- 内部类可以直接访问外部类的成员
内部类根据使用不同的修饰符或定义的位置不同,分成四种:
- 实例内部类:内部类没有使用static修饰
- 静态内部类:内部类使用了static修饰
- 局部内部类:在方法中定义的内部类
- 匿名内部类适合于仅使用一次使用的类,属于局部内部类的特殊情况
内部类看做是外部类的一个成员,好比字段,那么内部类可以使用public/缺省/protected/private修饰,还可以使用static修饰
外部类的访问修饰符,使用public,或者缺省
实例内部类:没有使用static修饰的内部类,属于外部类的对象,不属于外部类本身
实例内部类特点:
- 创建实例内部类前,必须存在外部类对象,通过外部类对象创建内部类对象(当存在内部类对象时,一定存在外部类对象)
- 实例内部类的实例自动持有外部类的实例的引用,内部类可以直接访问外部类成员
- 外部类中不能直接访问内部类的成员,必须通过内部类的实例去访问
- 实例内部类中不能定义静态成员,只能定义实例成员
- 如果实例内部类和外部类存在同名的字段或方法abc,那么在内部类中:
this.abc:表示访问内部类成员
外部类.this.abc:表示访问外部类成员
对于每个内部类来说,Java编译器会生成独立.class文件
成员内部类:外部类名$内部类名
局部内部类:外部类名$数字内部类名
匿名内部类:外部类名$数字
//外部类
class Outter
{
String name = "Outter.name";
public void ooxx()
{
System.out.println(this.new Inner().age); //3.外部类中不能直接访问内部类成员,必须通过内部类的实例去访问
}
//实例内部类
class Inner
{
int age = 17;
String name = "Inner.name";
public void test() //4.实例内部类中不能定义静态成员(不能加static),只能定义实例成员
{
//5.如果实例内部类和外部类存在同名的字段或方法,那么在内部类中:this.abc:表示访问内部类成员 外部类.this.abc:表示访问外部类成员
String name = "Local.name";
System.out.println(name); //Local.name
System.out.println(this.name); //Inner.name
System.out.println(Otter.this.name); //Outter.name
}
}
}
//演示实例内部类
class InstanceInnerClassDemo
{
public static void main(String[] args)
{
//1.创建实例内部类前,必须存在外部类对象,通过外部类对象创建内部类对象(当存在内部类对象时,一定存在外部类对象)
//创建Inner的对象
//Outter out = new Outter();
//Outter.Inner in = out.new Inner();
Outter.Inner in = new Outter().new Inner();
System.out.println(in);
//2.实例内部类的实例自动持有外部类的实例引用,内部类可以直接访问外部外部类成员
in.test();
}
}
静态内部类:
使用static修饰:
- 静态内部类的实例不会自动持有外部类的特定实例的引用,在创建内部类的实例时,不必创建外部类的实例
- 静态内部类可以直接访问外部类的静态成员,如果访问外部类的实例成员,必须通过外部类的实例去访问
- 在静态内部类中可以定义静态成员和实例成员
- 测试类可以通过完整类名直接访问静态内部类的静态成员
class Outter
{
String name = "Outter.name";
static String name2 = "name2";
static class Inner
{
static int age = 17; //定义静态成员和实例成员
//String name = "Inner.name";
public void test()
{
System.out.println(name2); //可以直接访问外部类的静态成员
System.out.println(new Outter().name); //访问外部类的实例成员,必须通过外部类的实例去访问
}
}
}
class InstanceInnerClassDemo
{
public static void main(String[] args)
{
Outter.Inner in = new Outter.Inner(); //实例不会自动持有外部类的特定实例的引用,在创建内部类的实例时,不必创建外部类的实例
in.test();
System.out.println( Outter.Inner.age); //通过完整类名直接访问静态内部类的静态成员
}
}
185、局部内部类分析
局部内部类(dasi不用):在方法中定义的内部类,其可见范围是当前方法和局部变量是同一个级别
- 不能使用public,private,protected,static修饰符
- 局部内部类只能在当前方法中使用
- 局部内部类和实例内部类一样,不能包含静态成员
- 局部内部类和实例内部类,可以访问外部类的所有成员
- 局部内部类访问局部变量必须使用final修饰(java8是自动隐式加上final,但依然是常量,不能改变值)。原因:如果当前方法不是main方法,那么当前方法调用完毕之后,当前方法的栈帧被销毁,方法内部的局部变量的空间全部被销毁,然后局部内部类是定义在方法中的,而且方法中会创建局部内部类对象,而局部内部类会去访问局部变量,当当前方法被销毁的时候,对象还在堆内存,依然持有对局部变量的引用,但是方法被销毁的时候局部变量已经被销毁了。
此时出现:在堆内存中,一个对象引用着一个不存在的数据。为了避免该问题,我们使用final修饰局部变量,从而变成常量,永驻内存空间,即使方法销毁后,该局部变量也在内存中,对象可以继续持有。
186、匿名内部类分析
匿名内部类(Anonymous),是一个没有名称的局部内部类,适合只使用一次的类。在开发中经常有这样的类,只需要定义一次,使用一次就可以丢弃了,此时,不应该白白定义在一个文件中。
在JavaSE/Android的事件处理中:不同的按钮点击之后,应该有不同的响应操作。首选匿名内部类。
- s匿名内部类本身没有构造器,但是会调用父类构造器
- 匿名内部类尽管没有构造器,但是还是可以在匿名类中提供一段实例初始化代码块,JVM在调用父类构造器后,会执行该段代码。
- 内部类处理可以继承类之外,还可以实现接口
格式:
new 父类构造器 ([实参列表]) 或接口 ()
{
//匿名内部类的类体部分
}
注意:匿名内部类必须继承一个父类或者实现一个接口,但最多只能一个父类或实现一个接口。
//安装USB键盘
//创建一个IUSB的匿名实现类对象
MotherBoard.pluginIn(new IUSB()
{
//匿名内部类体
public void swapData()
{
System.out.println("键盘在哪里");
}
}
);
(三)枚举
187、引入枚举和枚举的模拟
需求:定义一个Employee(员工),使用一个变量restday来表示他哪一天休息(一周的哪一天)
class Employee
{
private int restday; //一周的哪一天休息
public int getRestday()
{
return restday;
}
public void setRestday(int restday)
{
this.restday = restday;
}
}
//枚举的引入
class EnumDemo
{
public static void main(String[] args)
{
//创建一个员工对象,并设置他哪一天休息
Employee e = new Employee();
e.setRestday(3); //接受int类型的值
int restday = e.getRestday();
if (restday == 6 || restday == 7)
{
System.out.println("周末休息");
}
else
{
System.out.println("周一至周五休息");
}
}
}
上面的设计,感觉是没有问题,但是经不起推敲:使用int类型来表示星期几存在的问题:
- 类型不安全,完全可以设置非[1,7]之间的数
- 业务含义不够明确,设置的1表示周几?周一/周日?
解决方案:专门使用一个类weekday来表示
//定义一个星期几的常量类
class Weekday
{
public static final int MONDAY = 1;
public static final int TUESDAY = 2;
public static final int WEDNESDAY = 3;
public static final int THURSDAY = 4;
public static final int FRIDAY = 5;
public static final int SARTUDAY = 6;
public static final int SUNDAY = 7;
}
此时,调用代码:
//创建一个员工对象,并设置他哪一天休息
Employee e = new Employee();
e.setRestday(Weekday.WEDNESDAY); //周三
此时,业务含义很明确,因为Weekday.WEDNESDAY就是表示周三的意思,但是,,因为在Employee中的restday的类型是int类型,我们依然可以随意设置int类型的数据。如:e.setRestday(-13); 。所以还是没有解决数据类型不安全的问题。
解决方案:把休息日使用一个对象类型来表示,并固定该休息日的值只能是周一到周日
//定义一个星期几的常量类
class Weekday
{
private Weekday(){} //创建一个私有化构造器,防止外界new Weekday()
public static final Weekday MONDAY = new Weekday();
public static final Weekday TUESDAY = new Weekday();
public static final Weekday WEDNESDAY = new Weekday();
public static final Weekday THURSDAY = new Weekday();
public static final Weekday FRIDAY = new Weekday();
public static final Weekday SARTUDAY = new Weekday();
public static final Weekday SUNDAY = new Weekday();
}
class Employee
{
private Weekday restday; //一周的哪一天休息
public Weekday getRestday()
{
return restday;
}
public void setRestday(Weekday restday)
{
this.restday = restday;
}
}
//枚举的引入
class EnumDemo
{
public static void main(String[] args)
{
//创建一个员工对象,并设置他哪一天休息
Employee e = new Employee();
e.setRestday(Weekday.WEDNESDAY); //周三
//-------------------------------------------
Weekday restday = e.getRestday();
if (restday == Weekday.SARTUDAY || restday == Weekday.SUNDAY)
{
System.out.println("周末休息");
}
else
{
System.out.println("周一至周五休息");
}
}
}
188、枚举的定义和特点以及底层分析
枚举是从java5开始,提供的一种新的数据类型,是一个特殊的类,就是多个常量对象的集合。
格式:
[修饰符] enum 类名
{
常量A,常量B,常量C;s
}
//定义一个星期几的常量类
enum Weekday
{
MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SARTUDAY,SUNDAY; //枚举中的MONDAY就等价于public static final Weekday MONDAY = new Weekday();
}
class Employee
{
private Weekday restday; //一周的哪一天休息
public Weekday getRestday()
{
return restday;
}
public void setRestday(Weekday restday)
{
this.restday = restday;
}
}
//枚举的引入
class EnumDemo1
{
public static void main(String[] args)
{
//创建一个员工对象,并设置他哪一天休息
Employee e = new Employee();
e.setRestday(Weekday.WEDNESDAY); //周三
//-------------------------------------------
Weekday restday = e.getRestday();
if (restday == Weekday.SARTUDAY || restday == Weekday.SUNDAY)
{
System.out.println("周末休息");
}
else
{
System.out.println("周一至周五休息");
}
}
}
自定义的枚举类型,在编译(在底层)都是直接继承与java.lang.Enum类的,Enum是所有枚举的父类
枚举类的特点:
- 枚举的直接父类是java.lang.Enum,但是不能显示继承Enum
- 枚举就相当于一个类,可以定义构造方法、成员变量、普通方法和抽象方法
- 默认私有构造方法,即使不写访问权限也是private(假的构造器,底层没有无参数构造器)
- 每个实例分别用一个全局常量表示,枚举类的对象是固定的,实例个数有限,不能使用new关键字
- 枚举实例必须位于枚举体中的最开始部分,枚举实例列表的后面要有分号与其他成员相分隔
- 枚举实例后有花括号时,该实例是枚举类的匿名内部类对象(查看编译后的class文件)
189、枚举的操作细节
枚举的使用:
- 枚举中都是全局、公共的静态常量,可以直接使用枚举类名调用。如:Weekday day = Weekday.SARTUDAY;
- 因为java.lang.Enum类是所有枚举的父类,所以所有的枚举对象可以调用Enum类中的方法。
String name = 枚举对象.name(); //返回枚举对象的常量名称
int ordinal = 枚举对象.ordinal(); //返回枚举对象的序号,从0开始
String str = 枚举对象.toString(); //返回枚举对象的常量名称
3.编译器生成的枚举类的静态方法(从反编译代码中看):
枚举类型[] values(); // 返回当前枚举类型所有的常量,使用一个数组封装起来
枚举类型 valueOf(String name); //把一个指定名称的字符串,转换为当前枚举中同名的常量
4.从java5开始出现枚举,switch也支持操作枚举类型。switch只支持int类型,支持枚举是因为底层使用的枚举对象ordinal,而ordinal的类型依然是int类型。
枚举主要用来表示事物固定的类型
//定义一个星期几的常量类
enum Weekday
{
MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SARTUDAY,SUNDAY; //枚举中的MONDAY就等价于public static final Weekday MONDAY = new Weekday();
}
class Employee
{
private Weekday restday; //一周的哪一天休息
public Weekday getRestday()
{
return restday;
}
public void setRestday(Weekday restday)
{
this.restday = restday;
}
}
//枚举的引入
class EnumDemo1
{
public static void main(String[] args)
{
//创建一个员工对象,并设置他哪一天休息
Employee e = new Employee();
e.setRestday(Weekday.WEDNESDAY); //周三
//-------------------------------------------
Weekday restday = e.getRestday();
if (restday == Weekday.SARTUDAY || restday == Weekday.SUNDAY)
{
System.out.println("周末休息");
}
else
{
System.out.println("周一至周五休息");
}
//new Weekday(); // 无法实例化枚举类型
System.out.println(Weekday.SARTUDAY); //打印枚举
System.out.println(Weekday.SARTUDAY.name()); //SARTUDAY 返回枚举对象的常量名称
System.out.println(Weekday.SARTUDAY.ordinal()); //5 返回枚举对象的序号,从0开始
System.out.println(Weekday.SARTUDAY.toString()); //SARTUDAY 返回枚举对象的常量名称
System.out.println(Weekday.valueOf("MONDAY"));
Weekday[] ws = Weekday.values(); // 返回当前枚举类型所有的常量,使用一个数组封装起来
for (Weekday day :ws )
{
System.out.println(day);
}
switch (Weekday.SUNDAY)
{
case MONDAY : break;
case TUESDAY : break;
case WEDNESDAY : break;
}
}
}
枚举的单例模式:
在《effective java》书中提到,建议使用枚举做单例模式,很安全,即使使用反射也不能创建对象。
class ArrayUtil
{
//1)必须在该类中,自己先创建出一个对象
private static final ArrayUtil instance = new ArrayUtil();
//2)私有化自身的构造器,防止外界通过构造器创建新的对象
private ArrayUtil(){}
//3)向外暴露一个公共的静态方法用于获取自身的对象
public static ArrayUtil getInstance()
{
return instance;
}
//排序操作
public void sort(int[] arr)
{
System.out.println("排序操作");
}
}
//单例
class SingletionDemo
{
public static void main(String[] args)
{
System.out.println(ArrayUtil.getInstance() == ArrayUtil.getInstance());
//需要做排序:不同类中
ArrayUtil.getInstance().sort(null);
//需要做排序
ArrayUtil.getInstance().sort(null);
//需要做排序
ArrayUtil.getInstance().sort(null);
//需要做排序
ArrayUtil.getInstance().sort(null);
}
}
用枚举方式:
enum ArrayUtil
{
INSTANCE;
//排序操作
public void sort(int[] arr)
{
System.out.println("排序操作");
}
}
//单例
class SingletionDemo
{
public static void main(String[] args)
{
System.out.println(ArrayUtil.INSTANCE == ArrayUtil.INSTANCE); //true
//需要做排序:不同类中
ArrayUtil.INSTANCE.sort(null);
//需要做排序
ArrayUtil.INSTANCE.sort(null);
//需要做排序
ArrayUtil.INSTANCE.sort(null);
//需要做排序
ArrayUtil.INSTANCE.sort(null);
}
}
190、小结