很多前辈,老师,面试官考校Java菜鸟的时候时常会说:“面向对象的三大特性是什么?” ‘封装,继承,多态“,看过Java的各种教科书扉页的人相信都能轻松答出来吧。
但是所谓三特性是为何出现?为何如此重要?----------------------------------------------------------------
接口和内部类为我们提供了一种将接口和实现分离的更加结构化的方法
一、抽象类
作为一个抽象基类,他没有具体的意义,我们创建这个对象是希望通过这个通用接口操纵一系列类。
如果我们定义了一个抽象类,同时,在某些方法中,如果他在派生类中各个情况中所需要的操作不同,或者说是核心点,那么我么们就有必要将该方法设为抽象方法
abstract 返回值 方法名(参数集 参数1, ....);
如果从一个抽象类继承,那么必须实现基类的所有抽象方法。
之前的Instrument类就可以转化为抽象类。如下所示
abstract class Instrument {
private int i; // Storage allocated for each
public abstract void play(Note n);
public String what() { return "Instrument"; }
public abstract void adjust();
}
class Wind extends Instrument {
public void play(Note n) {
print("Wind.play() " + n);
}
public String what() { return "Wind"; }
public void adjust() {}
}
class Percussion extends Instrument {
public void play(Note n) {
print("Percussion.play() " + n);
}
public String what() { return "Percussion"; }
public void adjust() {}
}
class Stringed extends Instrument {
public void play(Note n) {
print("Stringed.play() " + n);
}
public String what() { return "Stringed"; }
public void adjust() {}
}
class Brass extends Wind {
public void play(Note n) {
print("Brass.play() " + n);
}
public void adjust() { print("Brass.adjust()"); }
}
class Woodwind extends Wind {
public void play(Note n) {
print("Woodwind.play() " + n);
}
public String what() { return "Woodwind"; }
}
public class Music4 {
// Doesn't care about type, so new types
// added to the system still work right:
static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
static void tuneAll(Instrument[] e) {
for(Instrument i : e)
tune(i);
}
public static void main(String[] args) {
// Upcasting during addition to the array:
Instrument[] orchestra = {
new Wind(),
new Percussion(),
new Stringed(),
new Brass(),
new Woodwind()
};
tuneAll(orchestra);
}
} /* Output:
Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C
*///:~
创建抽象类和抽象方法的主要原因是,他们可以是类的抽象性明确起来。并告诉用户和编译器如何来使用它们,抽象类是很有用的重构工具。因为他们使得我们可以将公共方法沿着继承层次结构向上移动
二、接口概念
一个接口表示,所有实现了该特定接口的类看起来都像这样。接口被用来建立类与类之间的协议。
1、接口本身的方法都是public的
2、接口的每一个方法都仅是而且必须是一个声明。
三、完全解耦
只要一个方法操作的是类而不是接口,那么你只能用这个类及其子类。如果你想要将这个方法应用于不在此继承结构上的某个类,那么那是不可能的。
但是接口很大程度上解决了这种限制。如下面的代码所举例
<pre name="code" class="java">import java.util.*;
class Processor {
public String name() {
return getClass().getSimpleName();
}
Object process(Object input) {
return input;
}
}
class Upcase extends Processor {
String process(Object input) { // Covariant return
return ((String) input).toUpperCase();
}
}
class Downcase extends Processor {
String process(Object input) {
return ((String) input).toLowerCase();
}
}
class Splitter extends Processor {
String process(Object input) {
// The split() argument divides a String into pieces:
return Arrays.toString(((String) input).split(" "));
}
}
public class Apply {
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 by definition incorrect";
public static void main(String[] args) {
process(new Upcase(), s);
process(new Downcase(), s);
process(new Splitter(), s);
}
}
/* Output:
Using Processor Upcase
DISAGREEMENT WITH BELIEFS IS BY DEFINITION INCORRECT
Using Processor Downcase
disagreement with beliefs is by definition incorrect
Using Processor Splitter
[Disagreement, with, beliefs, is, by, definition, incorrect]
*///:~
Apply.process()方法能够接受任何类型的Processor,并将其应用到一个Object对象中。
想这个例子一样。创建一个根据所传递的参数不同而具有不同行为的方法,被称为策略设计模式,这类方法包含所要执行的算法中固定不变的部分,而“策略”也将包含变化的部分。策略就是传递进去的参数对象,它包含要执行的代码。
Bruce给出了下面的例子:
如果我们将Processor改为一个接口会怎样?
public interface Processor {
String name();
Object process(Object input);
}
public class Apply{
public static void process(Processor p , Object s){
System.out.println("Using Processor" + p.name());
System.out.println(p.process(s));
}
复用代码的第一种方式就是遵循接口来编写自己的类 如下
import java.util.*;
public abstract class StringProcessor implements Processor{
//在这里首先声明一个抽象类来实现接口的大部分公共功能,作为基类供派生类继承。
public String name() {
return getClass().getSimpleName();
}
public abstract String process(Object input);
public static String s =
"If she weighs the same as a duck, she's made of wood";
public static void main(String[] args) {
Apply.process(new Upcase(), s);
Apply.process(new Downcase(), s);
Apply.process(new Splitter(), s);
}
}
class Upcase extends StringProcessor {
public String process(Object input) { //这样派生类的代码量将会明显减小
return ((String)input).toUpperCase();
}
}
class Downcase extends StringProcessor {
public String process(Object input) {
return ((String)input).toLowerCase();
}
}
class Splitter extends StringProcessor {
public String process(Object input) {
return Arrays.toString(((String)input).split(" "));
}
} /* Output:
Using Processor Upcase
IF SHE WEIGHS THE SAME AS A DUCK, SHE'S MADE OF WOOD
Using Processor Downcase
if she weighs the same as a duck, she's made of wood
Using Processor Splitter
[If, she, weighs, the, same, as, a, duck,, she's, made, of, wood]
*///:~
但是如果当我们出现一种情况。类库并非是我们所建立的,我们无法修改。在这种情况下,就要使用适配器设计模式,适配器中的代码将会接收所有的接口,并产生需要的接口,如下所示
class Filter {
public String name() {
return getClass().getSimpleName();
}
public Waveform process(Waveform input) { return input; }
}
class FilterAdapter implements Processor {
//他将实现Processor接口同时持有一个Fitler对象
Filter filter;
public FilterAdapter(Filter filter) {
this.filter = filter;
}
//并且在这里产生需要的方法,通过持有的对象来进行实现
public String name() { return 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(3.0, 4.0)), w);
}
} /* Output:
Using Processor LowPass
Waveform 0
Using Processor HighPass
Waveform 0
Using Processor BandPass
Waveform 0
*///:~
将接口从具体实现中解耦使得接口可以应用于多种不同的具体实现,因此代码也就更具可复用性
四、Java中的多重继承
由于接口没有任何的具体实现,也就是说,没有任何和接口有关的存储,因此多个接口的组合就可以实现。组合多个类的接口的行为被称为多重继承,这点C++的同学们应该不陌生。
在Java中实现的一个比较理想的方法就是由一个抽象基类或者具体基类来实现这些组合的接口12345678...,之后再由派生类决定具体的细节。例如:
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); // Treat it as a CanFight
u(h); // Treat it as a CanSwim
v(h); // Treat it as a CanFly
w(h); // Treat it as an ActionCharacter
}
} ///:~
之所以使用接口的核心原因。就是为了能够
上转型为多个基类型(灵活性),而第二个原因就是
防止其他程序员创建该类的对象。
事实上这正和抽象类有一些关系,我们如何选择?Bruce的话:【如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类。】
事实上,如果知道某事物应该成为一个基类,那么第一选择应当是是他成为一个接口。
五、通过继承来拓展接口
顾名思义 就是接口上extends接口。
六、适配接口
接口最神奇的地方就是允许同一个接口具有多个不同的具体实现,在同一个方法中,接口的实现和传递的对象则取决于方法的使用者。
因此一种常见的设计方法就是“策略设计模式”,准确的说,就是表达“你可以用任何你想要的对象来调用我的方法,只要你的对象遵循我的接口”。
例如下面就是一个实现与Scanner构造方法的参数的相同的Readable接口所写出的可以使Scanner正常工作的类
import java.nio.*;
import java.util.*;
public class RandomWords implements Readable {
private static Random rand = new Random(47);
private static final char[] capitals =
"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; // Indicates end of input
cb.append(capitals[rand.nextInt(capitals.length)]);
for(int i = 0; i < 4; i++) {
cb.append(vowels[rand.nextInt(vowels.length)]);
cb.append(lowers[rand.nextInt(lowers.length)]);
}
cb.append(" ");
return 10; // Number of characters appended
}
public static void main(String[] args) {
Scanner s = new Scanner(new RandomWords(10));
while(s.hasNext())
System.out.println(s.next());
}
} /* Output:
Yazeruyac
Fowenucor
Goeazimom
Raeuuacio
Nuoadesiw
Hageaikux
Ruqicibui
Numasetih
Kuuuuozog
Waqizeyoy
*///:~
上面的Readable要求实现read方法,在read内部,将输入内容添加进CharBuffer参数中。但是如果我们已经有一个输出类了,但是它没有实现Readable接口怎么让Scanner能够正常的使用它呢?
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.print(rd.next() + " ");
}
}
再次使用适配器设计模式,持有该对象同时实现Readable。
import java.nio.*;
import java.util.*;
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;
String result = Double.toString(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.print(s.nextDouble() + " ");
}
}
在这种方式中,我们可以在现有类之上添加新的接口,所以这意味着让方法接受接口类型,是一种让
任何类都可以对该方法进行适配的方式,这就是使用接口的强大之处
七、接口中的域
static和final的,并且不允许空final
八、嵌套接口
额……就像是内部类一样,不过我没找到它的使用价值。
九、接口和工厂
工厂方法设计模式,和直接调用构造器不同,在工厂对象上调用的是创建方法,而该工厂将生成某个实现的对象。
Bruce说道,理论上,我们的代码将完全与接口的实现分离,这就是我们可以透明的将某个实现替换为另一个实现。下面则是展示了工厂方法的架构
interface Service {
// 接口
void method1();
void method2();
}
interface ServiceFactory {
// 工厂方法接口
Service getService();
}
class Implementation1 implements Service {
// 實現類1
Implementation1() {
}
public void method1() {
System.out.println("Implementation1 method1");
}
public void method2() {
System.out.println("Implementation1 method2");
}
}
class Implementation1Factory implements ServiceFactory {
// 工廠方法的实现类,反悔了一个Service对象
public Service getService() {
return new Implementation1();
}
}
class Implementation2 implements Service {
Implementation2() {
} // Package access
public void method1() {
System.out.println("Implementation2 method1");
}
public void method2() {
System.out.println("Implementation2 method2");
}
}
class Implementation2Factory implements ServiceFactory {
public Service getService() {
return new Implementation2();
}
}
public class Factories {
public static void serviceConsumer(ServiceFactory fact) {
// 所以我们直接调用工厂的方法就可以得到对象,只需要声明工厂类别
Service s = fact.getService();
s.method1();
s.method2();
}
public static void main(String[] args) {
serviceConsumer(new Implementation1Factory());
serviceConsumer(new Implementation2Factory());
}
}
如果不是使用工厂模式的话,就必须制定创建的(实现类的)对象。但是为什么我们要创建这种间接性呢?答案是:框架
Bruce举例道:例如在相同的棋盘上下国际象棋和西洋跳棋
/**
* @describe 创建Game接口
*/
interface Game {
boolean move();
}
/**
* @describe 创建Game对象的工厂
*/
interface GameFactory {
Game getGame();
}
/**
* @describe 跳棋
*/
class Checkers implements Game {
private int moves = 0;
private static final int MOVES = 3;
public boolean move() {
System.out.println("Checkers move " + moves);
return ++moves != MOVES;
}
}
/**
* @describe 跳棋工厂,实现工厂模式,同时指定实现类模型
*/
class CheckersFactory implements GameFactory {
public Game getGame() {
return new Checkers();
}
}
class Chess implements Game {
private int moves = 0;
private static final int MOVES = 4;
public boolean move() {
System.out.println("Chess move " + moves);
return ++moves != MOVES;
}
}
class ChessFactory implements GameFactory {
public Game getGame() {
return new Chess();
}
}
public class Games {
/**
* @describe 在这里我们定义要获取的工厂就可以获得当前我们需要的对象了,而这都是泛用的Game对象
*/
public static void playGame(GameFactory factory) {
Game s = factory.getGame();
while (s.move())
;
}
public static void main(String[] args) {
playGame(new CheckersFactory());
playGame(new ChessFactory());
}
}
十、总结
任何的抽象性都是应真正的需求产生的。当必须时,应当重构接口而不是到处添加额外级别的间接性,由此带来的复杂性的提升。这无疑是不划算的。
而对于类和接口的选择,应当是优先选择类,而当接口的必须性变得非常明确,那么我们就将其抽象出来作为接口。接口是一种重要的工具,但是总是被滥用 —Bruce