Java课件笔记:sec2

sec2

1.抽象类(Abstract Classes)

(1)抽象类
Java抽象类是不能实例化的类,这意味着不能创建抽象类的新实例。抽象类的目的是作为子类的基。在Java中,通过向类声明中添加抽象关键字,可以声明类是抽象的。下面是一个Java抽象类示例:

public abstract class MyAbstractClass {

}
//这就是在Java中声明抽象类的全部内容,不能创建MyAbstractClass的实例

MyAbstractClass myClassInstance =new MyAbstractClass(); //not valid
//如果试图编译上面的代码,Java编译器将生成一个错误,说明不能实例化MyAbstractClass,
//因为它是一个抽象类

(2)抽象方法(Abstract Methods)
抽象类可以有抽象方法,通过在方法声明前面添加abstract关键字来声明抽象方法。下面是一个Java抽象方法示例:

public abstract class MyAbstractClass {
	public abstract void abstractMethod();
}

抽象方法没有实现,它只有一个方法签名,就像Java接口中的方法一样。如果一个类有一个抽象方法,那么整个类必须声明为抽象的,但并不是抽象类中的所有方法都必须是抽象方法,抽象类可以包含抽象方法和非抽象方法。
抽象类的子类必须实现(覆盖)其抽象超类的所有抽象方法。超类的非抽象方法只是继承它们本身。如果需要,还可以重写它们。下面是抽象类MyAbstractClass的一个子类示例:

public class MySubClass extends MyAbstractClass {
	public void abstractMethod() {
		System.out.println("My method implementation");
	}
}
//注意MySubClass必须如何从它的抽象超类MyAbstractClass实现抽象方法abstractMethod()

抽象类的子类不强制实现其超类的所有抽象方法的惟一情况是,子类也是抽象类。
(3)抽象类的作用
抽象类的目的是充当基类,基类可以由子类扩展来创建完整的实现。例如,假设某个过程需要3个步骤:
(i)操作前的一步。(ii)操作。(iii)操作后的步骤。
如果操作前后的步骤始终相同,那么可以使用此Java代码在抽象超类中实现3步流程。

public abstract class MyAbstractProcess {
	public void process() {
		stepBefore();
		action();
		stepAfter();
	}
	public void stepBefore() {} //直接在抽象超类中实现
	public abstract void action(); // 由子类实现
	public public void stepAfter() {} //直接在抽象超类中实现
}

注意action()方法是如何抽象的。MyAbstractProcess的子类现在可以扩展MyAbstractProcess并覆盖action()方法。调用子类的process()方法时,执行整个过程,包括抽象超类的步骤before()和步骤after(),以及子类的action()方法。
当然,MyAbstractProcess不必是抽象类才能作为基类来运行,action()方法也不必是抽象的,你可以用一个普通的类。但是,通过使方法实现抽象,从而实现类,您可以清楚地向该类的用户发出信号,即不应该按原样使用该类。相反,它应该用作子类的基类,抽象方法应该在子类中实现。
上面的示例没有action()方法的默认实现。在某些情况下,超类实际上可能具有子类应该覆盖的方法的默认实现。在这种情况下,不能使方法抽象。尽管如此,仍然可以使超类抽象,即使它不包含抽象方法。
下面是一个更具体的例子,它打开一个URL,处理它,然后关闭到该URL的连接:

public abstract class URLProcessorBase {
	public void process(URL url) throws IOException {
		URLConnection urlConnection = url.openConnection();
		InputStream input = urlConnection.getInputStream();
		try{
			processURLData(input);
		}
		nally {
			input.close();
		}
	}
	protected abstract void processURLData(InputStream input) throws IOException;
}

注意processURLData()是一个抽象方法,而URLProcessorBase是一个抽象类。URLProcessorBase的子类必须实现processURLData()方法,因为它是一个抽象方法。
URLProcessorBase抽象类的子类可以处理从URL下载的数据,而不用担心打开和关闭URL的网络连接。这是由URLProcessorBase完成的。子类只需要关心从传递给processURLData()方法的InputStream处理数据。这使得从url处理数据的类更容易实现。

public class URLProcessorImpl extends URLProcessorBase {
	@Override
	protected void processURLData(InputStream input) throws IOException {
		int data = input.read();
		while(data != -1){
			System.out.println((char) data);
			data = input.read();
		}
	}
}
//请注意子类如何实现processURLData()方法,仅此而已
//其余代码是从URLProcessorBase超类继承的

下面是一个如何使用URLProcessorImpl类的示例:

URLProcessorImpl urlProcessor = new URLProcessorImpl();
urlProcessor.process(new URL("http://jenkov.com"));

调用process()方法,该方法在URLProcessorBase超类中实现。这个方法反过来调用URLProcessorImpl类中的processURLData()。
上面用URLProcessorBase类向展示的示例实际上是模板方法设计模式的一个示例。模板方法设计模式提供了一些过程的部分实现,在扩展模板方法基类时可以完成这些过程的子类。

2.接口

(1)接口
Java接口有点像类,只不过Java接口只能包含方法签名和字段。Java接口不能包含方法的实现,只能包含方法的签名(名称、参数和异常)。

public interface MyInterface {
	public String hello = "Hello";
	public void sayHello();
}
//接口是使用interface关键字声明的
//就像类一样,Java接口可以声明为公共的或包范围(没有访问修饰符)

上面的接口示例包含一个变量和一个方法。可以直接从接口访问该变量,如下所示:

System.out.println(MyInterface.hello);

从接口访问变量与访问类中的静态变量非常相似,但是,该方法需要由某个类实现,然后才能访问它。
(2)实现一个接口
在真正使用接口之前,必须在某个Java类中实现该接口。这是一个实现上面所示的MyInterface接口的类:

public class MyInterfaceImpl implements MyInterface {
	public void sayHello() {
		System.out.println(MyInterface.hello);
	}
}

注意上面类声明的实现MyInterface部分。这向Java编译器发出信号,表示MyInterfaceImpl类实现了MyInterface接口。实现接口的类必须实现接口中声明的所有方法。方法必须具有与接口中声明的完全相同的签名(名称+参数)。该类不需要实现(声明)接口的变量,只有方法。
一旦Java类实现了Java接口,就可以使用该类的实例作为该接口的实例。这里有一个例子:

MyInterface myInterface = new MyInterfaceImpl();
	myInterface.sayHello();

不能单独创建Java接口的实例,必须始终创建实现该接口的类的实例,并将该实例引用为该接口的实例。
(3)实现多个接口
Java类可以实现多个Java接口。在这种情况下,类必须实现在所有实现的接口中声明的所有方法。这里有一个例子:

public class MyInterfaceImpl implements MyInterface, MyOtherInterface {
	public void sayHello() {
		System.out.println("Hello");
	}
	public void sayGoodbye() {
		System.out.println("Goodbye");
	}
}
//这个类实现了两个接口,分别是MyInterface和MyOtherInterface
//在implementation关键字之后列出要实现的接口的名称,用逗号分隔

如果接口不在与实现类相同的包中,则还需要导入接口。Java接口是使用导入指令导入的,就像Java类一样。例如:

import com.jenkov.package1.MyInterface;
import com.jenkov.package2.MyOtherInterface;
public class MyInterfaceImpl implements MyInterface, MyOtherInterface {
	...
}

下面是上述类实现的两个Java接口:

public interface MyInterface {
	public void sayHello();
}
public interface MyOtherInterface {
	public void sayGoodbye();
}
//每个接口都包含一个方法,这些方法由类MyInterfaceImpl实现

(4)重叠的方法签名
如果Java类实现了多个Java接口,那么其中一些接口可能包含具有相同签名(名称+参数)的方法。由于Java类只能实现具有给定签名的at方法一次,这可能会导致一些问题,而Java规范并没有对此问题提供任何解决方案。在那种情况下做什么由你决定。
(5)实现接口的类型
以下Java类型可以实现接口:Java Class、Java Abstract Class、Java Nested Class、Java Enum、Java Dynamic Proxy。
(6)接口变量
Java接口可以包含变量和常量,但是,在接口中放置变量通常没有意义。在某些情况下,在接口中定义常数是有意义的,特别是当实现接口的类使用这些常量时,例如在计算中,或者作为接口中某些方法的参数时。但建议是,如果可以的话,避免在Java接口中放置变量。接口中的所有变量都是公共的,即使在变量声明中省略了public关键字。
(6)接口方法
Java接口可以包含一个或多个方法声明。如前所述,接口不能为这些方法指定任何实现。由实现接口的类来指定实现。接口中的所有方法都是公共的,即使在方法声明中省略了public关键字。
(7)默认接口方法
Java 8之前,Java接口不能包含方法的实现,而只能包含方法签名。
(8)静态接口方法
Java接口可以有静态方法,Java接口中的静态方法必须具有实现。下面是Java接口中的静态方法的一个例子:

public interface MyInterface {
	public static void print(String text){
		System.out.print(text);
	}
}

在接口中调用静态方法看起来和工作起来就像在类中调用静态方法一样。下面是一个从上面的MyInterface接口调用静态print()方法的示例:

MyInterface.print("Hello static method!");

当希望提供一些实用程序方法时,接口中的静态方法可能是有用的,这些实用程序方法自然会进入与相同职责相关的接口。例如,车辆接口可以有printVehicle(Vehicle v)静态方法。
(9)接口和继承
Java接口可以从另一个Java接口继承,就像类可以从其他类继承一样。使用extends关键字指定继承。下面是一个简单的接口继承示例:

public interface MySuperInterface {
	public void saiHello();
}
public interface MySubInterface extends MySuperInterface {
	public void sayGoodbye();
}

接口MySubInterface扩展了接口MySuperInterface。这意味着,MySubInterface继承了MySuperInterface的所有字段和方法。这意味着,如果一个类实现了MySubInterface,那么这个类必须实现MySubInterface和MySuperInterface中指定的所有方法。
如果在设计中找到了想要的方法,那么可以在一个子接口中使用与超接口中的方法相同的签名(名称+参数)来定义方法。与类不同,接口实际上可以从多个超接口继承。可以通过列出要继承的所有接口的名称来指定,以逗号分隔。实现从多个接口继承的接口的类必须实现该接口及其超接口的所有方法。下面是一个从多个接口继承的Java接口示例:

public interface MySubInterface extends SuperInterface1, SuperInterface2 {
	public void sayItAll();
}

与实现多个接口时一样,当多个超接口具有具有相同签名(名称+参数)的方法时,没有规则规定如何处理这种情况。
(10)继承和默认方法
接口默认方法为接口继承规则增加了一点复杂性。虽然一个类通常可以实现多个接口,即使这些接口包含具有相同签名的方法,但是如果这些方法中有一个或多个是默认方法,那么这是不可能的。换句话说,如果两个接口包含相同的方法签名(名称+参数),其中一个接口将该方法声明为默认方法,那么类不能自动实现这两个接口。
如果接口扩展(从多个接口继承),并且其中一个或多个接口包含具有相同签名的方法,并且其中一个超接口声明重叠方法为默认方法,则情况相同。在上述两种情况下,Java编译器都要求实现接口的类显式地实现导致问题的方法。这样,类的实现就毫无疑问了。类中的实现优先于任何默认实现。
(11)接口和多态
Java接口是实现多态性的一种方法。多态性是一个需要一些实践和思考才能掌握的概念。基本上,多态性意味着类(对象)的实例可以当作不同类型的实例来使用。在这里,类型意味着类或接口。看看这个简单的类图:
在这里插入图片描述
上面的类都是模型的一部分,它们用字段和方法表示不同类型的车辆和司机。这就是这些类的责任——从现实生活中建模这些实体。
现在,假设需要能够将这些对象存储在数据库中,并将它们序列化为XML、JSON或其他格式,希望对每个操作使用单个方法实现该操作,每个Car、Truck或Vehicle对象都可以使用该方法。store()方法、serializeToXML()方法和serializeToJSON()方法。
请暂时忘记,将此功能作为直接在对象上的方法实现可能会导致混乱的类层次结构。想象一下,这就是您想要的操作实现方式。
解决这个问题的一种方法是为Vehicle和Driver类创建一个公共超类,该类具有存储和序列化方法。然而,这将导致概念上的混乱。类层次结构将不再建模车辆和驱动程序,而是绑定到应用程序中使用的存储和序列化机制。
更好的解决方案是创建一些带有存储和序列化方法的接口,并让类实现这些接口。下面是这些接口的示例:

public interface Storable {
	public void store();
}
public interface Serializable {
	public void serializeToXML(Writer writer);
	public void serializeToJSON(Writer writer);
}

当每个类实现这两个接口及其方法时,您可以通过将对象转换为接口类型的实例来访问这些接口的方法。您不需要确切地知道给定对象属于哪个类,只要知道它实现了什么接口即可。这里有一个例子:

Car car = new Car();
Storable storable = (Storable) car;
storable.store();
Serializable serializable = (Serializable) car;
serializable.serializeToXML (new FileWriter("car.xml"));
serializable.serializeToJSON(new FileWriter("car.json"));

正如您现在可能想象的那样,接口提供了一种比继承更清晰的方式来实现类中的横切功能。
(12)泛型接口
泛型Java接口是一种可以类型化的接口——这意味着在使用它时可以专门处理特定的类型(例如接口或类)。首先创建一个包含单个方法的简单Java接口:

public interface MyProducer() {
	public Object produce();
}

该接口表示一个接口,该接口包含一个名为generate()的方法,该方法可以生成一个对象。因为generate()的返回值是Object,所以它可以返回任何Java对象。这是一个实现MyProducer接口的类。

public class CarProducer implements MyProducer {
	public Object produce() {
		return new Car();
	}
}

上面的类CarProducer实现了MyProducer接口。generate()方法的实现在每次调用时返回一个新的Car对象。
下面是如何使用CarProducer类的:

MyProducer carProducer = new CarProducer();
Car car = (Car) carProducer.produce();

请注意,从carProducer.produce()方法调用返回的对象必须转换为Car实例,因为generate()方法返回类型是object。使用Java泛型,您可以键入MyProducer接口,以便在使用它时指定它生成的对象类型。
这是一个通用版本的MyProducer接口:

public interface MyProducer <T> {
public T produce();
}

现在,当在CarProducer类中实现MyProducer接口时,还必须包含泛型类型声明,如下所示:

public class CarProducer<T> implements MyProducer<T> {
	@Override
	public T produce() {
		return (T) new Car();
	}
}

现在,在创建CarProducer时,可以指定它的通用接口类型,如下所示:

MyProducer<Car> myCarProducer = new CarProducer<Car>();
Car produce = myCarProducer.produce();

正如你所看到的,因为CarProducer的泛型类型实例设置为汽车,不再需要把返回的对象产生()方法,因为原MyProducer接口中的方法声明,这个方法返回相同的类型是艾德的泛型类型时使用。
但是,现在实际上可以为CarProducer实例指定另一个泛型类型,而不是它从它的generate()方法实现中实际返回的类型。如果向上滚动,可以看到CarProducer.produce()实现返回Car对象,无论您在创建它时为它指定了什么泛型类型。因此,下面的声明是可能的,但是在执行时将返回ClassCastException。

MyProducer<String> myStringProducer = new CarProducer<String>();
String produce1 = myStringProducer.produce();

相反,当您在CarProducer类中实现MyProducer接口时,您可以锁定它的泛型类型。下面是一个在实现泛型接口时指定泛型接口类型的示例。

public class CarProducer implements MyProducer<Car> {
	@Override
	public Car produce() {
		return new Car();
	}
}

现在您不能在使用CarProducer时指定它的泛型类型。已经打到Car了。下面是如何使用CarProducer:

MyProducer<Car> myCarProducer = new CarProducer();
Car produce = myCarProducer.produce();

正如您所看到的,仍然没有必要强制转换generate()返回的对象,因为CarProducer实现将其声明为Car实例。
(13)函数式接口(Functional Interfaces)
从Java 8引入了一个新的概念,称为函数式接口。简而言之,函数式接口是具有单个未实现方法(非默认、非静态方法)的接口。函数式接口通常打算由Java Lambda表达式实现。
(14)接口vs.抽象类
Java接口用于将某些组件的接口与实现分离。换句话说,使使用接口的类独立于实现接口的类。因此,您可以交换接口的实现,而不必使用接口更改类。
抽象类通常用作子类扩展的基类。一些编程语言使用抽象类来实现多态性,并将接口与实现分离,但是在Java中,您使用接口来实现这一点。记住,Java类只能有一个超类,但是它可以实现多个接口。因此,如果一个类已经有一个不同的超类,那么它可以实现一个接口,但是不能扩展另一个抽象类。因此,接口是公开通用接口的一种更容易实现的机制。
如果需要将接口与其实现分离,请使用接口。如果还需要提供接口的基类或默认实现,请添加实现接口的抽象类(或普通类)。
下面的示例显示了引用接口的类、实现该接口的抽象类和扩展抽象类的子类。
在这里插入图片描述
下面是Java抽象类上的文本中的代码示例,但是添加了一个由抽象基类实现的接口。这样就像上面的图。

//First the interface:
public interface URLProcessor {
	public void process(URL url) throws IOException;
}

// Second, the abstract base class:
public abstract class URLProcessorBase implements URLProcessor {
	public void process(URL url) throws IOException {
		URLConnection urlConnection = url.openConnection();
		InputStream input = urlConnection.getInputStream();
		try {
			processURLData(input);
		}
		nally {
			input.close();
		}
	}
	protected abstract void processURLData(InputStream input) throws IOException;
}

//Third, the subclass of the abstract base class:
public class URLProcessorImpl extends URLProcessorBase {
	@Override
	protected void processURLData(InputStream input) throws IOException {
		int data = input.read();
		while(data != -1){
			System.out.println((char) data);
			data = input.read();
		}
	}
}

// Fourth, how to use the interface URLProcessor as variable type, even
//though it is the subclass UrlProcessorImpl that is instantiated.
URLProcessor urlProcessor = new URLProcessorImpl();
urlProcessor.process(new URL("http://jenkov.com"));

同时使用接口和抽象基类可以使代码更加灵活。只需子类化抽象基类,就可以实现简单的URL处理器。如果您需要更高级的东西,您的URL处理器可以直接实现URLProcessor接口,而不是从URLProcessorBase继承。

3.枚举(Enums)

Java Enum是一种特殊的Java类型,用于定义常量集合。更准确地说,Java枚举类型是一种特殊类型的Java类。枚举可以包含常量、方法等。Java 5中添加了Java枚举。
下面是一个简单的Java enum示例:

public enum Level {
	HIGH,
	MEDIUM,
	LOW,
}

请注意enum关键字用于替换类或接口。Java enum关键字向Java编译器发出信号,表明此类型的de nition是enum。可以像这样引用上面枚举中的常量:

Level level = Level.HIGH;

请注意,级别变量的类型级别就是上面示例中指定的Java枚举类型。级别变量可以将其中一个级别枚举常量作为值(高、中、低)。在本例中,level被设置为HIGH。
在这里插入图片描述

4.拉姆达表达式(Lambda Expressions)

Java lambda表达式是Java 8中的新特性。Java lambda表达式是Java进入函数式编程的第一步。因此,Java lambda表达式是一个可以在不属于任何类的情况下创建的函数。可以像传递对象一样传递Java lambda表达式并按需执行。
Java lambda表达式通常用于实现简单的事件监听器或回调,或用于Java Streams API的函数式编程。
函数式编程通常用于实现事件监听器。Java中的事件侦听器通常被定义为具有单个方法的Java接口。下面是ctive single method接口示例:

public interface StateChangeListener {
	public void onStateChange(State oldState, State newState);
}

这个Java接口定义了一个方法,每当状态发生变化时(无论观察到什么)就调用该方法。
在Java 7中,为了侦听状态更改,必须实现此接口。假设您有一个名为StateOwner的类,该类可以注册状态事件侦听器。这里有一个例子:

public class StateOwner {
	public void addStateListener(StateChangeListener listener) {
	}
}

在Java 7中,可以使用匿名接口实现添加事件侦听器,如下所示:

StateOwner stateOwner = new StateOwner();
stateOwner.addStateListener(new StateChangeListener() {
	public void onStateChange(State oldState, State newState) {
	// do something with the old and new state.
	}
}

首先创建一个状态所有者实例。然后,StateChangeListener接口的匿名实现作为侦听器添加到StateOwner实例上。
在Java 8中,可以使用Java lambda表达式添加事件侦听器,如下所示:

StateOwner stateOwner = new StateOwner();
stateOwner.addStateListener(
	(oldState, newState) - > System.out.println("State changed")
);

lambda expressions:

(oldState, newState) -> System.out.println("State changed")

lambda表达式与addStateListener()方法参数的参数类型相匹配。如果lambda表达式匹配参数类型(在本例中是StateChangeListener接口),则lambda表达式将转换为实现与该参数相同接口的函数。
Java lambda表达式只能在与之匹配的类型是单个方法接口的情况下使用。在上面的例子中,lambda表达式用作参数,其中参数类型是StateChangeListener接口。这个接口只有一个方法。因此,lambda表达式在该接口上成功匹配。

猜你喜欢

转载自blog.csdn.net/qq_41897243/article/details/85253256
sec