Java基础 -- 抽象类和接口
抽象类和接口都是用来为子类提供统一的接口,或者说是共同部分。
一 抽象类
Java提供了一种叫做抽象方法的机制,这种方法只有声明而没有方法体。下面是抽象方法声明所采用的语句:
1 | abstract void f(); |
包含抽象方法的类叫做抽象类,如果一个类包含一个或多个抽象方法,该类就是抽象类,抽象类要使用abstract修饰。
如果直接使用抽象类创建一个对象,则编译会发生错误;如果从一个抽象类继承,并想创建该新类的对象,那么就必须为抽象类中的所有抽象方法提供方法定义,如果不这么做,那么子类也是抽象类,且编译器会强制使用abstract关键字来限定这个类。
我们也可以创建一个没有抽象方法的抽象类。考虑这种情况:如果有一个类,让其包含任何abstract方法都显得没有实际意义,而且我们也想要阻止产生这个类的任何对象,那么这时这么做就很有用了。
下面演示一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | //抽象类 abstract class Instrument{ private int i; //抽象方法 public abstract void play(); public String what() { return "Instrument" ;} public abstract void adjust(); } class Wind extends Instrument{ public void play() { System.out.println( "Wind.play()" ); } public String what() { return "Wind" ;} public void adjust() {} } class Percussion extends Instrument{ public void play() { System.out.println( "Percussion.play()" ); } public String what() { return "Percussion" ;} public void adjust() {} } class Stringed extends Instrument{ public void play() { System.out.println( "Stringed.play()" ); } public String what() { return "Stringed" ;} public void adjust() {} } class Brass extends Instrument{ public void play() { System.out.println( "Brass.play()" ); } public String what() { return "Brass" ;} public void adjust() {} } class WoodWind extends Wind{ public void play() { System.out.println( "WoodWind.play()" ); } public String what() { return "WoodWind" ;} public void adjust() {} } public class Music { static void tune(Instrument i) { i.play(); } public static void main(String[] args) { Instrument[] orchestra = { new Wind(), new Percussion(), new Stringed(), new Brass(), new WoodWind() }; for ( int i= 0 ;i<orchestra.length;i++) { tune(orchestra[i]); } } } |
运行结果如下:
1 2 3 4 5 | Wind.play() Percussion.play() Stringed.play() Brass.play() WoodWind.play() |
二 接口
Interface关键字使抽象的概念更向前迈进了一步,abstract关键字允许人们在类中创建一个或多个没有任何定义的方法 -- 提供了接口部分,但是没有提供任何相应的具体实现,这些实现都是由此类的继承者来实现。
interface不仅仅是一个极度抽象的类,并且它还允许创建一个能够被向上转换为多种父类的类型,来实现某种类似多重继承变种的特性。
要创建一个接口,需要用Interface关键字来替换class关键字。就像类一样,可以在interface关键字之前使用public进行修饰(但是仅限于该接口在与其同名的文件中被定义)。如果不使用public关键字,则它默认是包访问权限,这样它就只能在一个包内可用。接口中方法都是public的(即使不显示声明)。接口也可以包含字段,但是这些字段隐式的是static和final的。
要让某一个类遵循某个特定接口(或者一组接口),需要使用implements关键字,它和继承有些类似。
下面演示一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | //接口 interface Instrument{ //static和final 编译时常量 int VALUE= 5 ; //只能是public(默认) void play(); void adjust(); } class Wind implements Instrument{ public void play() { System.out.println( "Wind.play()" ); } public String what() { return "Wind" ;} public void adjust() {} } class Percussion implements Instrument{ public void play() { System.out.println( "Percussion.play()" ); } public String what() { return "Percussion" ;} public void adjust() {} } class Stringed implements Instrument{ public void play() { System.out.println( "Stringed.play()" ); } public String toString() { return "Stringed" ;} public void adjust() {} } class Brass implements Instrument{ public void play() { System.out.println( "Brass.play()" ); } public String toString() { return "Brass" ;} public void adjust() {} } class WoodWind extends Wind{ public void play() { System.out.println( "WoodWind.play()" ); } public String toString() { return "WoodWind" ;} public void adjust() {} } public class Music { static void tune(Instrument i) { i.play(); } public static void main(String[] args) { Instrument[] orchestra = { new Wind(), new Percussion(), new Stringed(), new Brass(), new WoodWind() }; for ( int i= 0 ;i<orchestra.length;i++) { tune(orchestra[i]); } } } |
运行结果:
1 2 3 4 5 | Wind.play() Percussion.play() Stringed.play() Brass.play() WoodWind.play() |
三 完全解耦
如果有一个方法操作的是类而非接口(或者说一个方法需要传入一个类的对象引用为参数),那么你就只能使用这个类及其子类,如果你想要将这个方法作用于不再此继承结构中的某个类,那么编译器将会报错。接口在很大程度上可以放宽这种限制,因此,它使得我们可以编写可复用性更好的代码。
下面来看一个例子,假设有一个Processor类,它由一个name()方法,还有一个process()方法,该方法接收输入参数,修改它的值,然后输出。这个类别作为父类进行扩展,产生各种不同类型的Processor,在本例中,Processor的子类将作用于String对象上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | import java.util.Arrays; //父类 用于Object对象进行处理 class Processor{ public String name() { return getClass().getSimpleName(); } Object process(Object input) { return input;} } //子类 Object转换为大写字符串 class Upcase extends Processor{ String process(Object input) { return ((String)input).toUpperCase(); } } //子类 Object转换为小写字符串 class Downcase extends Processor{ String process(Object input) { return ((String)input).toLowerCase(); } } //子类 Object转换为数组 class Splitter extends Processor{ String process(Object input) { return Arrays.toString(((String)input).split( " " )); } } public class Apply { //该方法作用于Processor类及其子类 public static void process(Processor p,Object s) { System.out.println( "Using processor " + p.name()); System.out.println(p.process(s)); } public static String s = "Disagreement with beliefs is my definition incorrect" ; public static void main(String[] args) { process( new Upcase(),s); process( new Downcase(),s); process( new Splitter(),s); } } |
输出结果为:
1 2 3 4 5 6 | Using processor Upcase DISAGREEMENT WITH BELIEFS IS MY DEFINITION INCORRECT Using processor Downcase disagreement with beliefs is my definition incorrect Using processor Splitter [Disagreement, with, beliefs, is, my, definition, incorrect] |
Apply.process()方法可以接受任何类型的Processor,并将其应用到一个Object对象上,然后输出结果。在上面这个例子中,创建一个能够根据所传递的参数对象的不同而具有不同行为的方法,被称为策略设计模式。这类方法包含所要指向的算法中固定不变的部分,而”策略“包含变化的部分。策略就是传递进去的参数对象,它包含要执行的代码。这里Processor对象就是一个策略,在main()中可以看到有三种不同的类型应用到了String类型的s对象上。
下面我们再来看一个例子,现在假设我们发现了一组电子滤波器,它们看起来好像适用于Apply.process()方法(即这组电子滤波器和process()的参数Processor具有相同的接口)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | class Waveform{ public static long counter; private final long id = counter++; public String toString() { return "Waveform " + id;} } public class Filter { public String name() { return getClass().getSimpleName(); } Waveform process(Waveform input) { return input;} } class LowPass extends Filter{ double cutoff; public LowPass( double cutoff) { this .cutoff = cutoff;} public Waveform process(Waveform input) { return input; } } class HighPass extends Filter{ double cutoff; public HighPass( double cutoff) { this .cutoff = cutoff;} public Waveform process(Waveform input) { return input; } } class BandPass extends Filter{ double lowCutoff,highCutoff; public BandPass( double lowCutoff, double highCutoff) { this .lowCutoff = lowCutoff; this .highCutoff = highCutoff; } public Waveform process(Waveform input) { return input; } } |
Filter和Processor具有相同的接口元素,但是由于Filter并非继承自Processor,因此不能将Filter用于Apply.process()方法上。这里主要是因为Apply.process()方法和Processor之间的耦合过紧,已经超出了所需要的程度,这就使得应该复用Apply.process()的代码时,复用却被禁止了。
如何解决这个问题呢,如果Processor是一个接口,那么这些限制就会变得松动,使得Filter用于Apply.process()方法上。下面是修改版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | import java.util.Arrays; //接口 interface Processor{ public String name(); public Object process(Object input); } class Apply { //该方法作用于Processor类及其子类 public static void process(Processor p,Object s) { System.out.println( "Using processor " + p.name()); System.out.println(p.process(s)); } } /****** 复用代码第一种方式是客户端程序员遵循该接口来编写它们自己的类 */ public abstract class StringProcessor implements Processor{ public String name() { return getClass().getSimpleName(); } //这句不能少 public abstract String process(Object input); public static String s = "Disagreement with beliefs is my definition incorrect" ; public static void main(String[] args) { Apply.process( new Upcase(),s); Apply.process( new Downcase(),s); Apply.process( new Splitter(),s); } } //子类 Object转换为大写字符串 class Upcase extends StringProcessor{ public String process(Object input) { return ((String)input).toUpperCase(); } } //子类 Object转换为小写字符串 class Downcase extends StringProcessor{ public String process(Object input) { return ((String)input).toLowerCase(); } } //子类 Object转换为数组 class Splitter extends StringProcessor{ public String process(Object input) { return Arrays.toString(((String)input).split( " " )); } } |
输出结果:
1 2 3 4 5 6 | Using processor Upcase DISAGREEMENT WITH BELIEFS IS MY DEFINITION INCORRECT Using processor Downcase disagreement with beliefs is my definition incorrect Using processor Splitter [Disagreement, with, beliefs, is, my, definition, incorrect] |
但是大多数情况下是你无法修改你想要使用的类。例如,在电子滤波器的例子中,假设该类库是别人写好的,你只能使用,无法修改Filter,但是Apply.process()需要的是Processor类型的参数。在这种情况下,可以使用适配器设计模式。适配器中的代码将接受你所拥有的接口(Filter),并产生你所需要的接口(Processor),就像下面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | //适配器设计模式 class FilterAdapter implements Processor{ Filter filter; public FilterAdapter(Filter filter) { this .filter = filter; } public String name() { return this .filter.name(); } public Waveform process(Object input) { return filter.process((Waveform)input); } } public class FilterProcessor{ public static void main(String[] args) { Waveform w = new Waveform(); Apply.process( new FilterAdapter( new LowPass( 1.0 )), w); Apply.process( new FilterAdapter( new HighPass( 2.0 )), w); Apply.process( new FilterAdapter( new BandPass( 1.0 , 2.0 )), w); } } |
输出如下:
1 2 3 4 5 6 | Using processor LowPass Waveform 0 Using processor HighPass Waveform 0 Using processor BandPass Waveform 0 |
四 Java中的多重继承
在Java中如果想要从一个非接口的类继承,那么只能从一个非接口的类继承,但是可以同时继承多个接口。并且需要将所有的接口名都置于implements关键字之后,用逗号将他们一一隔开。
下面的例子展示一个具体类:继承自一个非接口类和几个接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | interface CanFight{ void fight(); } interface CanSwim{ void swim(); } interface CanFly{ void fly(); } class ActionCharacter{ public void fight() {} } class Hero extends ActionCharacter implements CanFight,CanSwim,CanFly{ public void swim() {} public void fly() {} } public class Adventure { public static void t(CanFight x) {x.fight();} public static void u(CanSwim x) {x.swim();;} public static void v(CanFly x) {x.fly();;} public static void w(ActionCharacter x) {x.fight();} public static void main(String[] args) { Hero h = new Hero(); t(h); u(h); v(h); w(h); } } |
可以看到Hero继承自具体的类ActionCharacter和接口CanFight、CanSwim、CanFly。当通过这种方式将一个具体类和多个接口组合到一起时,这个具体类必须放到前面,后面跟着的才是接口。
注意:CanFight接口与ActionCharacter类中的fight()方法的特征签名是一样的,而且Hero中并没有提供fight()的定义。当想要创建对象时,所有的定义首先必须都存在。即使Hero没有显式的提供fight()的定义,其定义也会因ActionCharacter而随之而来,这样就使得创建Hero对象成为了可能。
五 通过继承来扩展接口
通过继承,可以很容易的在接口中添加新的方法声明,还可以通过继承在新接口中组合数个接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | interface Monster{ void menace(); } interface DangerousMonster extends Monster{ void destory(); } interface Lethal{ void kill(); } class DragonZilla implements DangerousMonster{ public void menace() {} public void destory() {} } interface Vampire extends DangerousMonster,Lethal{ void drinkBlood(); } class VerBadVampire implements Vampire{ public void menace() {} public void destory() {} public void kill() {} public void drinkBlood() {}; } public class HorrorShow { static void u(Monster b) {b.menace();} static void v(DangerousMonster d) { d.menace(); d.destory(); } static void w(Lethal l) {l.kill();} public static void main(String[] args) { DangerousMonster barney = new DragonZilla(); u(barney); v(barney); Vampire vlad = new VerBadVampire(); u(vlad); v(vlad); w(vlad); } } |
六 适配接口
接口最吸引人的原因之一就是允许同一个接口具有多个不同的具体实现。在简单的情况中,它的体现形式通常是一个接受接口类型的方法,可以接收如下参数:该接口或该接口的具体实现。
因此,接口的一种常见用法就是前面介绍到的策略设计模式,假设我们编写了一个执行某些操作的方法,而该方法将接受一个接口。只要我们创建一个遵循该接口的对象,就可以使用该方法来调用。
例如:Java SE5的Scanner类的构造器接受的就是一个Readable接口,因此只要我们创建一个类使其遵循Readable接口,就可以使Scanner作用于该类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | import java.util.*; import java.nio.*; public class RandomWords implements Readable{ private static Random rand = new Random( 47 ); private static final char [] captials = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" .toCharArray(); private static final char [] lowers = "abcdefghijklmnopqrstuvwxyz" .toCharArray(); private static final char [] vowels = "aeiou" .toCharArray(); private int count; public RandomWords( int count) { this .count = count;} public int read(CharBuffer cb) { if (count-- == 0 ) { return - 1 ; } cb.append( this .captials[rand.nextInt( this .captials.length)]); for ( int i= 0 ;i< 4 ;i++) { cb.append( this .vowels[rand.nextInt( this .vowels.length)]); cb.append( this .lowers[rand.nextInt( this .lowers.length)]); } cb.append( " " ); return 10 ; } public static void main(String[] args) { Scanner s = new Scanner( new RandomWords( 10 )); while (s.hasNext()) { System.out.println(s.next()); } } } |
输出结果:
1 2 3 4 5 6 7 8 9 10 | Yazeruyac Fowenucor Goeazimom Raeuuacio Nuoadesiw Hageaikux Ruqicibui Numasetih Kuuuuozog Waqizeyoy |
Readable接口只需要实现read()方法,在read()函数内部,将输入内容添加到CharBuffer参数中,或者在没有输入时返回-1。
假设有一个未实现Readable接口的类(RandomDoubles类),怎么才能让Scanner作用于它呢?下面演示一个使用适配接口的案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import java.util.*; public class RandomDoubles { private static Random rand = new Random( 47 ); public double next() { return rand.nextDouble();} public static void main(String[] args) { RandomDoubles rd = new RandomDoubles(); for ( int i= 0 ; i < 7 ;i++) { System.out.println(rd.next() + "" ); } } } |
输出如下:
1 2 3 4 5 6 7 | 0.7271157860730044 0.5309454508634242 0.16020656493302599 0.18847866977771732 0.5166020801268457 0.2678662084200585 0.2613610344283964 |
适配的类可以通过继承和实现Readable接口来实现,因此通过使用interface关键字提供的伪多重继承机制,可以生成既是RandomDoubles又是Readable的新类,下面我们将创建一个适配器AdaptedRandomDoubles类,该类继承于实现类RandomDoubles和接口Readable,从而使得Scanner可以作用于AdaptedRandomDoubles类,进而作用于RandomDoubles类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import java.util.*; import java.nio.*; public class AdaptedRandomDoubles extends RandomDoubles implements Readable{ private int count; public AdaptedRandomDoubles( int count) { this .count = count;} public int read(CharBuffer cb) { if (count-- == 0 ) { return - 1 ; } //产生一个double类型数 String result = Double.toString( this .next()) + " " ; cb.append(result); return result.length(); } public static void main(String[] args) { Scanner s = new Scanner( new AdaptedRandomDoubles( 7 )); while (s.hasNextDouble()) { System.out.println(s.nextDouble() + " " ); } } } |
亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。
日期 | 姓名 | 金额 |
---|---|---|
2023-09-06 | *源 | 19 |
2023-09-11 | *朝科 | 88 |
2023-09-21 | *号 | 5 |
2023-09-16 | *真 | 60 |
2023-10-26 | *通 | 9.9 |
2023-11-04 | *慎 | 0.66 |
2023-11-24 | *恩 | 0.01 |
2023-12-30 | I*B | 1 |
2024-01-28 | *兴 | 20 |
2024-02-01 | QYing | 20 |
2024-02-11 | *督 | 6 |
2024-02-18 | 一*x | 1 |
2024-02-20 | c*l | 18.88 |
2024-01-01 | *I | 5 |
2024-04-08 | *程 | 150 |
2024-04-18 | *超 | 20 |
2024-04-26 | .*V | 30 |
2024-05-08 | D*W | 5 |
2024-05-29 | *辉 | 20 |
2024-05-30 | *雄 | 10 |
2024-06-08 | *: | 10 |
2024-06-23 | 小狮子 | 666 |
2024-06-28 | *s | 6.66 |
2024-06-29 | *炼 | 1 |
2024-06-30 | *! | 1 |
2024-07-08 | *方 | 20 |
2024-07-18 | A*1 | 6.66 |
2024-07-31 | *北 | 12 |
2024-08-13 | *基 | 1 |
2024-08-23 | n*s | 2 |
2024-09-02 | *源 | 50 |
2024-09-04 | *J | 2 |
2024-09-06 | *强 | 8.8 |
2024-09-09 | *波 | 1 |
2024-09-10 | *口 | 1 |
2024-09-10 | *波 | 1 |
2024-09-12 | *波 | 10 |
2024-09-18 | *明 | 1.68 |
2024-09-26 | B*h | 10 |
2024-09-30 | 岁 | 10 |
2024-10-02 | M*i | 1 |
2024-10-14 | *朋 | 10 |
2024-10-22 | *海 | 10 |
2024-10-23 | *南 | 10 |
2024-10-26 | *节 | 6.66 |
2024-10-27 | *o | 5 |
2024-10-28 | W*F | 6.66 |
2024-10-29 | R*n | 6.66 |
2024-11-02 | *球 | 6 |
2024-11-021 | *鑫 | 6.66 |
2024-11-25 | *沙 | 5 |
2024-11-29 | C*n | 2.88 |

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了