本文是我们名为“ 高级Java ”的学院课程的一部分。
本课程旨在帮助您最有效地使用Java。 它讨论了高级主题,包括对象创建,并发,序列化,反射等。 它将指导您完成Java掌握的过程! 在这里查看 !
目录
- 1.简介 2.接口 3.标记接口 4.功能接口,默认方法和静态方法 5.抽象类 6.不可变的类 7.匿名类 8.可见度 9.继承 10.多重继承 11.继承与组成 12.封装 13.期末课程和方法 14.接下来 15.下载源代码
1.简介
不管您使用哪种编程语言(这里Java也不例外),遵循良好的设计原则是编写简洁,可理解,可测试的代码并提供长期有效且易于维护的解决方案的关键因素。 在本教程的这一部分中,我们将讨论Java语言提供的基础构建块,并介绍一些设计原则,以帮助您做出更好的设计决策。
更确切地说,我们将讨论与默认的方法 接口和接口 (Java 8的新功能), 抽象和final 类,不可变类,继承,组成和重温了一下可见性 (或可访问)的规则,我们有部分短暂触及教程1 , 如何创建和销毁对象 。
2.接口
在面向对象的编程中,接口的概念构成了契约驱动(或基于契约)开发的基础。 简而言之,接口定义方法的集合(契约),并且声称支持该特定接口的每个类都必须提供这些方法的实现:一个非常简单但功能强大的想法。
许多编程语言确实具有一种形式或另一种形式的接口,但是Java特别为此提供了语言支持。 让我们看一下Java中的简单接口定义。
package com.javacodegeeks.advanced.design;public interface SimpleInterface {void performAction();
}
在上面的代码片段中,我们命名为SimpleInterface
的接口SimpleInterface
声明了一个名为performAction
方法。 接口相对于类的主要区别在于接口概述了联系方式(声明方法),但未提供其实现。
但是,Java中的接口可能比这更复杂:它们可以包括嵌套的接口,类,枚举,注释(该枚举和注释将在本教程的第5部分中详细介绍如何以及何时使用Enums和Annotations )和常量。 。 例如:
package com.javacodegeeks.advanced.design;public interface InterfaceWithDefinitions {String CONSTANT = "CONSTANT";enum InnerEnum {E1, E2;}class InnerClass {}interface InnerInterface {void performInnerAction();}void performAction();
}
在这个更复杂的示例中,存在一些关于嵌套构造和方法声明的接口隐式施加的约束,而Java编译器则强制执行这些约束。 首先,即使没有明确说明,该接口中的每个声明都是公共的 (并且只能是public ,有关可见性和可访问性规则的更多详细信息,请参阅Visibility部分)。 因此,以下方法声明是等效的:
public void performAction();
void performAction();
值得一提的是,接口中的每个方法都隐式声明为抽象 ,甚至这些方法声明也是等效的:
public abstract void performAction();
public void performAction();
void performAction();
至于常量字段声明,除了是public
,它们是隐式static
和final
因此以下声明也等效:
String CONSTANT = "CONSTANT";
public static final String CONSTANT = "CONSTANT";
最后,除了public
之外,嵌套类,接口或枚举还隐式声明为static
。 例如,这些类声明也等效:
class InnerClass {
}static class InnerClass {
}
您将选择哪种样式是个人喜好,但是了解这些简单的界面质量可以使您避免不必要的键入。
3.标记接口
标记接口是一种特殊的接口,没有定义任何方法或其他嵌套结构。 在本教程的第2部分中 ,我们已经看到了标记接口的一个示例,该接口使用所有对象共有的方法 ,即Cloneable
接口。 这是在Java库中定义的方式:
public interface Cloneable {
}
标记接口本身并不是契约,而是某种有用的技术,用于“附加”或“绑定”类的某些特定特征。 例如,对于Cloneable
,该类被标记为可用于克隆,但是它应该或可以做的方式不是接口的一部分。 标记接口的另一个非常著名且广泛使用的示例是Serializable
:
public interface Serializable {
}
此接口将类标记为可用于序列化和反序列化,并且再次,它未指定可以或应该完成的方式。
标记器接口虽然不能满足作为合同的接口的主要目的,但在面向对象的设计中仍占有一席之地。
4.功能接口,默认方法和静态方法
随着Java 8的发布 ,接口获得了非常有趣的新功能:静态方法,默认方法和lambda(功能性接口)的自动转换。
在接口一节中,我们强调了以下事实:Java中的接口只能声明方法,而不能提供其实现。 使用默认方法不再是事实:接口可以使用default
关键字标记方法并为其提供实现。 例如:
package com.javacodegeeks.advanced.design;public interface InterfaceWithDefaultMethods {void performAction();default void performDefaulAction() {// Implementation here}
}
作为实例级别,默认方法可以被每个接口实现者覆盖,但是从现在开始,接口还可以包括static
方法,例如:
package com.javacodegeeks.advanced.design;public interface InterfaceWithDefaultMethods {static void createAction() {// Implementation here}
}
可能有人说在接口中提供实现会破坏基于契约的开发的全部目的,但是将这些功能引入Java语言有很多原因,无论它们多么有用或令人困惑,它们都可以为您提供帮助采用。
功能接口是另外一个故事,并且事实证明它们是该语言的非常有用的附加组件。 基本上,功能接口是仅声明了一个抽象方法的接口。 Java标准库中的Runnable
接口就是这个概念的一个很好的例子:
@FunctionalInterface
public interface Runnable {void run();
}
Java编译器以不同的方式对待功能接口,并且能够将lambda函数转换为有意义的功能接口实现。 让我们看一下以下函数定义:
public void runMe( final Runnable r ) {r.run();
}
要在Java 7及以下版本中调用此函数,应提供Runnable
接口的实现(例如,使用Anonymous类 ),但是在Java 8中,使用lambda语法传递run()
方法实现就足够了:
runMe( () -> System.out.println( "Run!" ) );
此外, @FunctionalInterface
批注(批注将在本教程的第5部分“ 如何以及何时使用Enums和Annotations”中详细介绍 )提示编译器验证接口仅包含一种抽象方法,以便对接口中引入的任何更改未来将不会打破这一假设。
5.抽象类
Java语言支持的另一个有趣的概念是抽象类的概念。 抽象类在某种程度上类似于Java 7中的接口,并且与Java 8中具有默认方法的接口非常接近。与常规类相比,抽象类无法实例化,但可以被子类化(请参阅继承一节以获取更多详细信息)。 更重要的是,抽象类可能包含抽象方法:没有实现的特殊方法,就像接口一样。 例如:
package com.javacodegeeks.advanced.design;public abstract class SimpleAbstractClass {public void performAction() {// Implementation here}public abstract void performAnotherAction();
}
在此示例中,类SimpleAbstractClass
被声明为abstract
并且还具有一个abstract
方法声明。 当实现细节的大部分甚至一部分可以被许多子类共享时,抽象类非常有用。 但是,它们仍然敞开了大门,并允许通过抽象方法自定义每个子类的固有行为。
值得一提的是,与只能包含public
声明的接口相反,抽象类可以使用可访问性规则的全部功能来控制抽象方法的可见性(有关更多详细信息,请参见“ 可见性和继承 ”部分)。
6.不可变的类
不可变性在当今的软件开发中变得越来越重要。 多核系统的兴起引起了很多与数据共享和并发相关的担忧(在第9部分 , 并发最佳实践中 ,我们将详细讨论那些主题)。 但是,肯定会出现一件事:可变状态的减少(甚至不存在)导致更好的可伸缩性和系统的简化推理。
不幸的是,Java语言没有为类不变性提供强大的支持。 但是,结合使用多种技术,可以设计不可变的类。 首先,该类的所有字段都应该是final
。 这是一个好的开始,但不能单独保证不变性。
package com.javacodegeeks.advanced.design;import java.util.Collection;public class ImmutableClass {private final long id;private final String[] arrayOfStrings;private final Collection< String > collectionOfString;
}
其次,遵循适当的初始化:如果字段是对集合或数组的引用,请不要直接从构造函数参数中分配这些字段,而应创建副本。 这将确保集合或数组的状态不会从外部更改。
public ImmutableClass( final long id, final String[] arrayOfStrings,final Collection< String > collectionOfString) {this.id = id;this.arrayOfStrings = Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );this.collectionOfString = new ArrayList<>( collectionOfString );
}
最后,提供适当的访问器(获取器)。 对于集合,不可变视图应使用Collections.unmodifiableXxx
包装器公开。
public Collection<String> getCollectionOfString() {return Collections.unmodifiableCollection( collectionOfString );
}
对于数组,确保真正的不变性的唯一方法是提供副本,而不是返回对数组的引用。 从实际的角度来看,这可能是不可接受的,因为它在很大程度上取决于数组的大小,并可能给垃圾收集器带来很大压力。
public String[] getArrayOfStrings() {return Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
}
即使是这个很小的例子,也提供了一个很好的主意,即不变性还不是Java中的一等公民。 如果一个不可变的类具有引用另一个类实例的字段,那么事情就会变得非常复杂。 这些类也应该是不可变的,但是没有简单的方法可以强制执行。
有很多很棒的Java源代码分析器,例如FindBugs和PMD ,它们可以通过检查代码并指出常见的Java编程缺陷来提供很多帮助。 这些工具是任何Java开发人员的好朋友。
7.匿名类
在Java 8之前的时代,匿名类是提供就地类定义和即时实例化的唯一方法。 匿名类的目的是减少样板,并提供简洁明了的方式将类表示为表达式。 让我们看一下在Java中产生新线程的典型老式方法:
package com.javacodegeeks.advanced.design;public class AnonymousClass {public static void main( String[] args ) {new Thread(// Example of creating anonymous class which implements// Runnable interfacenew Runnable() {@Overridepublic void run() {// Implementation here}}).start();}
}
在此示例中, Runnable
接口的实现作为匿名类就地提供。 尽管匿名类存在一些限制,但使用它们的基本缺点是Java强制将其作为语言使用的语法结构非常冗长。 即使是最简单的不执行任何操作的匿名类,每次都至少需要编写5行代码。
new Runnable() {@Overridepublic void run() {}}
幸运的是,有了Java 8,lambda和功能接口,所有这些样板都将消失,最终使Java代码看起来非常简洁。
package com.javacodegeeks.advanced.design;public class AnonymousClass {public static void main( String[] args ) {new Thread( () -> { /* Implementation here */ } ).start();}
}
8.可见度
在本教程的第1部分“ 如何设计类和接口”中 ,我们已经讨论了Java可见性和可访问性规则。 在这一部分中,我们将再次回到该主题,但要涉及子类化。
修饰符 | 包 | 子类 | 其他所有人 |
上市 | 无障碍 | 无障碍 | 无障碍 |
受保护的 | 无障碍 | 无障碍 | 无法访问 |
<无修饰符> | 无障碍 | 无法访问 | 无法访问 |
私人的 | 无法访问 | 无法访问 | 无法访问 |
表格1
不同的可见性级别允许或不允许这些类查看其他类或接口(例如,如果它们位于不同的程序包中或彼此嵌套)或子类查看和访问其父级的方法,构造函数和字段。
在下一节Inheritance中 ,我们将看到它的作用。
9.继承
继承是面向对象编程的关键概念之一,它是建立类关系的基础。 与可见性和可访问性规则结合使用,继承可以设计可扩展和可维护的类层次结构。
从概念上讲,Java中的继承是通过使用子类和extends
关键字以及父类来实现的。 子类继承其父类的所有公共成员和受保护成员。 此外,如果子类都驻留在同一包中,则它们继承父类的包私有成员。 话虽如此,无论您要设计什么,保持类公开或对其子类公开的最小方法集都是非常重要的。 例如,让我们看一下Parent
类及其子类Child
以演示不同的可见性级别及其效果:
package com.javacodegeeks.advanced.design;public class Parent {// Everyone can see itpublic static final String CONSTANT = "Constant";// No one can access itprivate String privateField;// Only subclasses can access itprotected String protectedField;// No one can see itprivate class PrivateClass {}// Only visible to subclassesprotected interface ProtectedInterface {}// Everyone can call itpublic void publicAction() {}// Only subclass can call itprotected void protectedAction() {}// No one can call itprivate void privateAction() {}// Only subclasses in the same package can call itvoid packageAction() {}
}
package com.javacodegeeks.advanced.design;// Resides in the same package as parent class
public class Child extends Parent implements Parent.ProtectedInterface {@Overrideprotected void protectedAction() {// Calls parent's method implementationsuper.protectedAction();}@Overridevoid packageAction() {// Do nothing, no call to parent's method implementation}public void childAction() {this.protectedField = "value";}
}
继承本身就是一个非常大的主题,其中包含许多特定于Java的细微细节。 但是,有一些易于遵循的规则可以对保持类层次结构简洁有很大帮助。 在Java中,每个子类都可以覆盖其父类的任何继承的方法,除非将其声明为final
(请参阅Final类和方法部分 )。
但是,没有特殊的语法或关键字来将方法标记为已重写,这可能会引起很多混乱。 这就是引入@Override
批注的原因:每当您打算覆盖继承的方法时,请始终使用@Override
批注进行指示。
Java开发人员在设计中经常面临的另一个难题是构建类层次结构(使用具体或抽象类)与接口实现。 强烈建议尽可能使用接口而不是类或抽象类。 接口轻巧得多,易于测试(使用模拟)和维护,并且使实现更改的副作用最小化。 许多高级编程技术(例如在标准Java库中创建类代理)都严重依赖于接口。
10.多重继承
与C ++和其他一些语言相比,Java不支持多重继承:在Java中,每个类都只有一个直接父级( Object
类位于层次结构的顶部,正如我们在本教程的第2部分中已经知道的那样, 使用通用方法)。到所有对象 )。 但是,该类可以实现多个接口,因此,堆叠接口是在Java中实现(或模仿)多重继承的唯一方法。
package com.javacodegeeks.advanced.design;public class MultipleInterfaces implements Runnable, AutoCloseable {@Overridepublic void run() {// Some implementation here}@Overridepublic void close() throws Exception {// Some implementation here}
}
多个接口的实现实际上是非常强大的,但是重用实现的需求通常会导致深层次的类层次结构,这是克服Java中缺少多继承支持的一种方法。
public class A implements Runnable {@Overridepublic void run() {// Some implementation here}
}
// Class B wants to inherit the implementation of run() method from class A.
public class B extends A implements AutoCloseable {@Overridepublic void close() throws Exception {// Some implementation here}
}
// Class C wants to inherit the implementation of run() method from class A
// and the implementation of close() method from class B.
public class C extends B implements Readable {@Overridepublic int read(java.nio.CharBuffer cb) throws IOException {// Some implementation here}
}
依此类推...最近的Java 8版本通过引入默认方法在某种程度上解决了该问题。 由于使用默认方法,因此接口实际上已经开始不仅提供合同,而且还提供实现。 因此,实现这些接口的类也将自动继承这些实现的方法。 例如:
package com.javacodegeeks.advanced.design;public interface DefaultMethods extends Runnable, AutoCloseable {@Overridedefault void run() {// Some implementation here}@Overridedefault void close() throws Exception {// Some implementation here}
}// Class C inherits the implementation of run() and close() methods from the
// DefaultMethods interface.
public class C implements DefaultMethods, Readable {@Overridepublic int read(java.nio.CharBuffer cb) throws IOException {// Some implementation here}
}
请注意,多重继承是一种强大的工具,但同时又是一种危险的工具。 通常将众所周知的“死亡钻石”问题称为多重继承实现的基本缺陷,因此,敦促开发人员非常仔细地设计类层次结构。 不幸的是,具有默认方法的Java 8接口也正成为这些漏洞的受害者。
interface A {default void performAction() {}
}interface B extends A {@Overridedefault void performAction() {}
}interface C extends A {@Overridedefault void performAction() {}
}
例如,以下代码片段无法编译:
// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}
在这一点上,可以说Java作为一种语言总是试图逃避面向对象编程的极端情况,但是随着语言的发展,其中一些情况开始出现。
11.继承与组成
幸运的是,继承不是设计类的唯一方法。 许多开发人员认为比继承更好的另一种选择是组合。 这个想法很简单:这些类应该由其他类组成,而不是建立类层次结构。
让我们看一下这个例子:
public class Vehicle {private Engine engine;private Wheels[] wheels;// ...
}
Vehicle
类由engine
和wheels
(加上许多其他零件,为简单起见而保留)。 但是,可以说Vehicle
类也是一种引擎,因此可以使用继承进行设计。
public class Vehicle extends Engine {private Wheels[] wheels;// ...
}
哪个设计决定是正确的? 一般准则称为IS-A和HAS-A原则。 IS-A是继承关系:子类还满足父类规范以及父类的此类IS-A变体。 因此, HAS-A是组成关系:该类拥有(或HAS-A )属于它的对象。 在大多数情况下, HAS-A原理比IS-A更好,其原因有两个:
- 设计可以更改的方式更加灵活
- 该模型更加稳定,因为更改不会通过类层次结构传播
- 与继承紧密关联父级及其子类的继承相比,该类及其组合之间的关联松散
- 关于类的推理更加简单,因为所有相关性都包含在其中
但是,继承有其自己的位置,以不同的方式解决了实际的设计问题,因此不应忽略。 在设计面向对象的模型时,请牢记这两种选择。
12.封装
面向对象编程中的封装概念全是向外界隐藏实现细节(如状态,内部方法等)。 封装的好处是可维护性和易于更改。 内在细节类暴露的越少,开发人员对更改其内部实现的控制就越多,而不必担心破坏现有代码(如果您正在开发许多人使用的库或框架,这将是一个真正的问题)。
Java中的封装是使用可见性和可访问性规则实现的。 在Java中,最好不要使用getter和setter方法(如果未将字段声明为final
)直接暴露字段,这是Java的最佳实践。 例如:
package com.javacodegeeks.advanced.design;public class Encapsulation {private final String email;private String address;public Encapsulation( final String email ) {this.email = email;}public String getAddress() {return address;}public void setAddress(String address) {this.address = address;}public String getEmail() {return email;}
}
此示例类似于Java语言中所谓的JavaBeans
:通过遵循一组约定编写的常规Java类,其中一种仅允许使用getter和setter方法访问字段。
正如我们在“ 继承”部分中已强调的那样,请始终遵循封装原则,尝试使类公共契约保持最小。 无论什么都不应该是public
,而应该是private
(或protected
/ package private
,这取决于您要解决的问题)。 从长远来看,它将带来回报,使您可以自由开发设计而无需引入重大更改(或至少将更改减至最少)。
13.期末课程和方法
在Java中,有一种方法可以防止该类被其他任何类继承:应该将其声明为final
。
package com.javacodegeeks.advanced.design;public final class FinalClass {
}
方法声明中使用相同的final
关键字可防止所讨论的方法在子类中被覆盖。
package com.javacodegeeks.advanced.design;public class FinalMethod {public final void performAction() {}
}
没有通用的规则来决定类或方法是否应该是final
。 最终类和方法限制了可扩展性,并且很难预先考虑该类是否应该被子类化,或者方法是否应该被覆盖。 这对于库开发人员尤其重要,因为这样的设计决策可能会大大限制库的适用性。
Java标准库提供了一些final
类的示例,其中最著名的是String
类。 在早期阶段,已做出决定来主动防止任何开发人员尝试提出自己的“更好的”字符串实现。
14.接下来
在本教程的这一部分中,我们研究了Java中的面向对象设计概念。 我们还简要地介绍了基于合同的开发,介绍了一些功能概念,并了解了该语言如何随着时间演变。 在本教程的下一部分中,我们将讨论泛型以及泛型如何改变我们处理类型安全编程的方式。
15.下载源代码
这是关于如何设计类和接口的课程。
- 您可以在此处下载源代码: advanced-java-part-3
翻译自: https://www.javacodegeeks.com/2015/09/how-to-design-classes-and-interfaces.html