ホームページ  >  記事  >  Java  >  Java コードを記述するための 4 つのヒントを要約する

Java コードを記述するための 4 つのヒントを要約する

不言
不言オリジナル
2018-08-25 16:05:481271ブラウズ

この記事は、Java コードを記述するための 4 つのテクニックをまとめたものです。必要な方は参考にしていただければ幸いです。

私たちの通常のプログラミングタスクは、同じテクノロジースイートを異なるプロジェクトに適用することに他なりません。ほとんどの場合、これらのテクノロジーは目標を達成できます。ただし、プロジェクトによっては特殊なテクノロジーの使用が必要な場合があるため、エンジニアはさらに深く掘り下げて、最もシンプルで効果的な方法を見つける必要があります。この記事では、一般的な問題の解決に役立ついくつかの一般的な設計戦略と目標実装テクニックを紹介します:

  1. 目的のある最適化のみを行う

  2. 定数には可能な限り列挙型を使用する

  3. 内部で再定義するクラス equals() メソッド

  4. 可能な限りポリモーフィズムを使用してください

この記事で説明されている手法は、すべての状況に適しているわけではないことに注意してください。さらに、これらのテクノロジーをいつ、どこで使用するかについては、ユーザーが慎重に検討する必要があります。

1. 目的のある最適化のみを実行します

大規模なソフトウェア システムは、パフォーマンスの問題を非常に考慮する必要があります。最も効率的なコードを作成したいと考えていますが、多くの場合、コードを最適化したい場合、開始する方法がありません。たとえば、次のコードはパフォーマンスに影響しますか?

public void processIntegers(List<Integer> integers) {

for (Integer value: integers) {
    for (int i = integers.size() - 1; i >= 0; i--) {
        value += integers.get(i);
    }
}
}

状況によって異なります。上記のコードから、その処理アルゴリズムは O(n³) (big O 表記を使用) であることがわかります。ここで、n はリスト コレクションのサイズです。 n が 5 のみの場合、問題はなく、25 回の反復だけが実行されます。ただし、n が 100,000 の場合、パフォーマンスに影響する可能性があります。その場合でも問題が発生するかどうかは保証できませんのでご了承ください。この方法では 10 億回の論理反復を実行する必要がありますが、パフォーマンスに影響があるかどうかはまだ議論の余地があります。

たとえば、クライアントがこのコードを独自のスレッドで実行し、計算が完了するまで非同期で待機すると仮定すると、その実行時間は許容範囲内である可能性があります。同様に、システムが運用環境にデプロイされているが、呼び出しを行うクライアントがない場合は、システム全体のパフォーマンスをまったく消費しないため、このコードを最適化する必要はまったくありません。実際、パフォーマンスを最適化した後、システムはより複雑になりますが、その結果、システムのパフォーマンスが向上しないという悲劇があります。

最も重要なことは、世界にはフリーランチがないということです。そのため、コストを削減するために、通常、キャッシュ、ループ拡張、または事前計算された値などのテクノロジーを介して最適化を実装します。これにより、実際にはシステムの複雑さが増加します。コードの可読性を削減します。この最適化によってシステムのパフォーマンスが向上するのであれば、それが複雑になっても価値はありますが、決定を下す前に、まず次の 2 つの情報を理解しておく必要があります:

  1. パフォーマンス要件は何ですか

  2. パフォーマンスのボトルネックはどこですか?

まず、パフォーマンス要件が何であるかを明確に知る必要があります。最終的に要件内に収まり、エンドユーザーが異議を唱えない場合には、パフォーマンスの最適化を実行する必要はありません。しかし、新たな機能が追加されたり、システムのデータ量がある程度の規模に達した場合には、最適化を行わないと問題が発生する可能性があります。

この場合、直感や検査に頼るべきではありません。なぜなら、Martin Fowler のような経験豊富な開発者でも、リファクタリング (70 ページ) の記事で説明されているように、間違った最適化を行う傾向があるからです。

十分なプログラムを分析すると、パフォーマンスに関する興味深い点は、ほとんどの場合、システムのごく一部で無駄になります。すべてのコードに対して同じ最適化を実行すると、最適化されたコードはそれほど頻繁に実行されないため、最終的には最適化の 90% が無駄になります。目標を持たずに最適化に費やす時間は時間の無駄です。

経験豊富な開発者として、私たちはこの視点を真剣に受け止める必要があります。最初の推測ではシステムのパフォーマンスが向上しないだけでなく、開発時間の 90% が完全に無駄になります。代わりに、実稼働環境 (または実稼働前環境) で一般的なユースケースを実行し、実行中にシステムのどの部分がシステム リソースを消費しているかを調べてから、システムを構成する必要があります。たとえば、コードの 10% だけがほとんどのリソースを消費する場合、コードの残りの 90% を最適化するのは時間の無駄です。

分析結果によると、この知識を活用するには、最も一般的な状況から始める必要があります。これにより、実際の努力によって最終的にシステムのパフォーマンスが向上することが保証されるからです。各最適化の後、分析ステップを繰り返す必要があります。これにより、システムのパフォーマンスが確実に向上するだけでなく、システムを最適化した後にパフォーマンスのボトルネックがどこにあるのかもわかります (1 つのボトルネックが解決された後、他のボトルネックがシステム全体のリソースをより多く消費する可能性があるため)。残りのボトルネックは一時的に変化しないため、既存のボトルネックで費やされる時間の割合は増加する可能性が高く、ターゲットのボトルネックが解消されると全体の実行時間は減少するはずであることに注意してください。

尽管在Java系统中想要对概要文件进行全面检查需要很大的容量,但是还是有一些很常见的工具可以帮助发现系统的性能热点,这些工具包括JMeter、AppDynamics和YourKit。另外,还可以参见DZone的性能监测指南,获取更多关于Java程序性能优化的信息。

虽然性能是许多大型软件系统一个非常重要的组成部分,也成为产品交付管道中自动化测试套件的一部分,但是还是不能够盲目的且没有目的的进行优化。相反,应该对已经掌握的性能瓶颈进行特定的优化。这不仅可以帮助我们避免增加了系统的复杂性,而且还让我们少走弯路,不去做那些浪费时间的优化。

2.常量尽量使用枚举

需要用户列出一组预定义或常量值的场景有很多,例如在web应用程序中可能遇到的HTTP响应代码。最常见的实现技术之一是新建类,该类里面有很多静态的final类型的值,每个值都应该有一句注释,描述该值的含义是什么:

public class HttpResponseCodes {
public static final int OK = 200;
public static final int NOT_FOUND = 404;
public static final int FORBIDDEN = 403;
}
if (getHttpResponse().getStatusCode() == HttpResponseCodes.OK) {
// Do something if the response code is OK 
}

能够有这种思路就已经非常好了,但这还是有一些缺点:

没有对传入的整数值进行严格的校验
由于是基本数据类型,因此不能调用状态代码上的方法
在第一种情况下只是简单的创建了一个特定的常量来表示特殊的整数值,但并没有对方法或变量进行限制,因此使用的值可能会超出定义的范围。例如:

public class HttpResponseHandler {
public static void printMessage(int statusCode) {
    System.out.println("Recieved status of " + statusCode); 
}
}

HttpResponseHandler.printMessage(15000);
尽管15000并不是有效的HTTP响应代码,但是由于服务器端也没有限制客户端必须提供有效的整数。在第二种情况下,我们没有办法为状态代码定义方法。例如,如果想要检查给定的状态代码是否是一个成功的代码,那就必须定义一个单独的函数:

public class HttpResponseCodes {
public static final int OK = 200;
public static final int NOT_FOUND = 404;
public static final int FORBIDDEN = 403;
public static boolean isSuccess(int statusCode) {
    return statusCode >= 200 && statusCode < 300; 
}
}
if (HttpResponseCodes.isSuccess(getHttpResponse().getStatusCode())) {
// Do something if the response code is a success code 
}

为了解决这些问题,我们需要将常量类型从基本数据类型改为自定义类型,并只允许自定义类的特定对象。这正是Java枚举(enum)的用途。使用enum,我们可以一次性解决这两个问题:

public enum HttpResponseCodes {
OK(200),
FORBIDDEN(403),
NOT_FOUND(404);
private final int code; 
HttpResponseCodes(int code) {
    this.code = code;
}
public int getCode() {
    return code;
}
public boolean isSuccess() {
    return code >= 200 && code < 300;
}
}
if (getHttpResponse().getStatusCode().isSuccess()) {
// Do something if the response code is a success code 
}

同样,现在还可以要求在调用方法的时候提供必须有效的状态代码:

public class HttpResponseHandler {
public static void printMessage(HttpResponseCode statusCode) {
    System.out.println("Recieved status of " + statusCode.getCode()); 
}
}

HttpResponseHandler.printMessage(HttpResponseCode.OK);
值得注意的是,举这个例子事项说明如果是常量,则应该尽量使用枚举,但并不是说什么情况下都应该使用枚举。在某些情况下,可能希望使用一个常量来表示某个特殊值,但是也允许提供其它的值。例如,大家可能都知道圆周率,我们可以用一个常量来捕获这个值(并重用它):

public class NumericConstants {
public static final double PI = 3.14;
public static final double UNIT_CIRCLE_AREA = PI * PI;
}
public class Rug {
private final double area;
public class Run(double area) {
    this.area = area;
}
public double getCost() {
    return area * 2;
}
}
// Create a carpet that is 4 feet in diameter (radius of 2 feet)
Rug fourFootRug = new Rug(2 * NumericConstants.UNIT_CIRCLE_AREA);

因此,使用枚举的规则可以归纳为:

当所有可能的离散值都已经提前知道了,那么就可以使用枚举

再拿上文中所提到的HTTP响应代码为例,我们可能知道HTTP状态代码的所有值(可以在RFC 7231中找的到,它定义了HTTP 1.1协议)。因此使用了枚举。在计算圆周率的情况下,我们不知道关于圆周率的所有可能值(任何可能的double都是有效的),但同时又希望为圆形的rugs创建一个常量,使计算更容易(更容易阅读);因此定义了一系列常量。

如果不能提前知道所有可能的值,但是又希望包含每个值的字段或方法,那么最简单的方法就是可以新建一个类来表示数据。尽管没有说过什么场景应该绝对不用枚举,但要想知道在什么地方、什么时间不使用枚举的关键是提前意识到所有的值,并且禁止使用其他任何值。

3.重新定义类里面的equals()方法

对象识别可能是一个很难解决的问题:如果两个对象在内存中占据相同的位置,那么它们是相同的吗?如果它们的id相同,它们是相同的吗?或者如果所有的字段都相等呢?虽然每个类都有自己的标识逻辑,但是在系统中有很多西方都需要去判断是否相等。例如,有如下的一个类,表示订单购买…

public class Purchase {
private long id;
public long getId() {
    return id;
}
public void setId(long id) {
    this.id = id;
}
}

……就像下面写的这样,代码中肯定有很多地方都是类似于的:

Purchase originalPurchase = new Purchase();
Purchase updatedPurchase = new Purchase();
if (originalPurchase.getId() == updatedPurchase.getId()) {
// Execute some logic for equal purchases 
}

这些逻辑调用的越多(反过来,违背了DRY原则),Purchase类的身份信息也会变得越来越多。如果出于某种原因,更改了Purchase类的身份逻辑(例如,更改了标识符的类型),则需要更新标识逻辑所在的位置肯定也非常多。

我们应该在类的内部初始化这个逻辑,而不是通过系统将Purchase类的身份逻辑进行过多的传播。乍一看,我们可以创建一个新的方法,比如isSame,这个方法的入参是一个Purchase对象,并对每个对象的id进行比较,看看它们是否相同:

public class Purchase {
private long id;
public boolean isSame(Purchase other) {
    return getId() == other.gerId();   
}
}

虽然这是一个有效的解决方案,但是忽略了Java的内置功能:使用equals方法。Java中的每个类都是继承了Object类,虽然是隐式的,因此同样也就继承了equals方法。默认情况下,此方法将检查对象标识(内存中相同的对象),如JDK中的对象类定义(version 1.8.0_131)中的以下代码片段所示:

public boolean equals(Object obj) {
return (this == obj);
}

这个equals方法充当了注入身份逻辑的自然位置(通过覆盖默认的equals实现):

public class Purchase {
private long id;
public long getId() {
    return id;
}
public void setId(long id) {
    this.id = id;
}
@Override
public boolean equals(Object other) {
    if (this == other) {
        return true;
    }
    else if (!(other instanceof Purchase)) {
        return false;
    }
    else {
        return ((Purchase) other).getId() == getId();
    }
}
}

虽然这个equals方法看起来很复杂,但由于equals方法只接受类型对象的参数,所以我们只需要考虑三个案例:

另一个对象是当前对象(即originalPurchase.equals(originalPurchase)),根据定义,它们是同一个对象,因此返回true

另一个对象不是Purchase对象,在这种情况下,我们无法比较Purchase的id,因此,这两个对象不相等

其他对象不是同一个对象,但却是Purchase的实例,因此,是否相等取决于当前Purchase的id和其他Purchase是否相等

现在可以重构我们之前的条件,如下:

Purchase originalPurchase = new Purchase();
Purchase updatedPurchase = new Purchase();
if (originalPurchase.equals(updatedPurchase)) {
// Execute some logic for equal purchases 
}

除了可以在系统中减少复制,重构默认的equals方法还有一些其它的优势。例如,如果构造一个Purchase对象列表,并检查列表是否包含具有相同ID(内存中不同对象)的另一个Purchase对象,那么我们就会得到true值,因为这两个值被认为是相等的:

List<Purchase> purchases = new ArrayList<>();
purchases.add(originalPurchase);
purchases.contains(updatedPurchase); // True

通常,无论在什么地方,如果需要判断两个类是否相等,则只需要使用重写过的equals方法就可以了。如果希望使用由于继承了Object对象而隐式具有的equals方法去判断相等性,我们还可以使用= =操作符,如下:

if (originalPurchase == updatedPurchase) {
// The two objects are the same objects in memory 
}

还需要注意的是,当equals方法被重写以后,hashCode方法也应该被重写。有关这两种方法之间关系的更多信息,以及如何正确定义hashCode方法,请参见此线程。

正如我们所看到的,重写equals方法不仅可以将身份逻辑在类的内部进行初始化,并在整个系统中减少了这种逻辑的扩散,它还允许Java语言对类做出有根据的决定。

4.尽量多使用多态性

对于任何一门编程语言来说,条件句都是一种很常见的结构,而且它的存在也是有一定原因的。因为不同的组合可以允许用户根据给定值或对象的瞬时状态改变系统的行为。假设用户需要计算各银行账户的余额,那么就可以开发出以下的代码:

public enum BankAccountType {
CHECKING,
SAVINGS,
CERTIFICATE_OF_DEPOSIT;
}
public class BankAccount {
private final BankAccountType type;
public BankAccount(BankAccountType type) {
    this.type = type;
}
public double getInterestRate() {
    switch(type) {
        case CHECKING:
            return 0.03; // 3%
        case SAVINGS:
            return 0.04; // 4%
        case CERTIFICATE_OF_DEPOSIT:
            return 0.05; // 5%
        default:
            throw new UnsupportedOperationException();
    }
}
public boolean supportsDeposits() {
    switch(type) {
        case CHECKING:
            return true;
        case SAVINGS:
            return true;
        case CERTIFICATE_OF_DEPOSIT:
            return false;
        default:
            throw new UnsupportedOperationException();
    }
}
}

虽然上面这段代码满足了基本的要求,但是有个很明显的缺陷:用户只是根据给定帐户的类型决定系统的行为。这不仅要求用户每次要做决定之前都需要检查账户类型,还需要在做出决定时重复这个逻辑。例如,在上面的设计中,用户必须在两种方法都进行检查才可以。这就可能会出现失控的情况,特别是接收到添加新帐户类型的需求时。

我们可以使用多态来隐式地做出决策,而不是使用账户类型用来区分。为了做到这一点,我们将BankAccount的具体类转换成一个接口,并将决策过程传入一系列具体的类,这些类代表了每种类型的银行帐户:

public interface BankAccount {
public double getInterestRate();
public boolean supportsDeposits();
}
public class CheckingAccount implements BankAccount {
@Override
public double getIntestRate() {
    return 0.03;
}
@Override
public boolean supportsDeposits() {
    return true;
}
}
public class SavingsAccount implements BankAccount {
@Override
public double getIntestRate() {
    return 0.04;
}
@Override
public boolean supportsDeposits() {
    return true;
}
}
public class CertificateOfDepositAccount implements BankAccount {
@Override
public double getIntestRate() {
    return 0.05;
}
@Override
public boolean supportsDeposits() {
    return false;
}
}

这不仅将每个帐户特有的信息封装到了到自己的类中,而且还支持用户可以在两种重要的方式中对设计进行变化。首先,如果想要添加一个新的银行帐户类型,只需创建一个新的具体类,实现了BankAccount的接口,给出两个方法的具体实现就可以了。在条件结构设计中,我们必须在枚举中添加一个新值,在两个方法中添加新的case语句,并在每个case语句下插入新帐户的逻辑。

其次,如果我们希望在BankAccount接口中添加一个新方法,我们只需在每个具体类中添加新方法。在条件设计中,我们必须复制现有的switch语句并将其添加到我们的新方法中。此外,我们还必须在每个case语句中添加每个帐户类型的逻辑。

在数学上,当我们创建一个新方法或添加一个新类型时,我们必须在多态和条件设计中做出相同数量的逻辑更改。例如,如果我们在多态设计中添加一个新方法,我们必须将新方法添加到所有n个银行帐户的具体类中,而在条件设计中,我们必须在我们的新方法中添加n个新的case语句。如果我们在多态设计中添加一个新的account类型,我们必须在BankAccount接口中实现所有的m数,而在条件设计中,我们必须向每个m现有方法添加一个新的case语句。

虽然我们必须做的改变的数量是相等的,但变化的性质却是完全不同的。在多态设计中,如果我们添加一个新的帐户类型并且忘记包含一个方法,编译器会抛出一个错误,因为我们没有在我们的BankAccount接口中实现所有的方法。在条件设计中,没有这样的检查,以确保每个类型都有一个case语句。如果添加了新类型,我们可以简单地忘记更新每个switch语句。这个问题越严重,我们就越重复我们的switch语句。我们是人类,我们倾向于犯错误。因此,任何时候,只要我们可以依赖编译器来提醒我们错误,我们就应该这么做。

关于这两种设计的第二个重要注意事项是它们在外部是等同的。例如,如果我们想要检查一个支票帐户的利率,条件设计就会类似如下:

BankAccount checkingAccount = new BankAccount(BankAccountType.CHECKING);
System.out.println(checkingAccount.getInterestRate()); // Output: 0.03

相反,多态设计将类似如下:

BankAccount checkingAccount = new CheckingAccount();
System.out.println(checkingAccount.getInterestRate()); // Output: 0.03

从外部的角度来看,我们只是在BankAccount对象上调用getintereUNK()。如果我们将创建过程抽象为一个工厂类的话,这将更加明显:

public class ConditionalAccountFactory {
public static BankAccount createCheckingAccount() {
     return new BankAccount(BankAccountType.CHECKING);
}
}
public class PolymorphicAccountFactory {
public static BankAccount createCheckingAccount() {
     return new CheckingAccount();
}
}
// In both cases, we create the accounts using a factory
BankAccount conditionalCheckingAccount = ConditionalAccountFactory.createCheckingAccount();
BankAccount polymorphicCheckingAccount = PolymorphicAccountFactory.createCheckingAccount();
// In both cases, the call to obtain the interest rate is the same
System.out.println(conditionalCheckingAccount.getInterestRate()); // Output: 0.03
System.out.println(polymorphicCheckingAccount.getInterestRate()); // Output: 0.03

将条件逻辑替换成多态类是非常常见的,因此已经发布了将条件语句重构为多态类的方法。这里就有一个简单的例子。此外,马丁·福勒(Martin Fowler)的《重构》(p . 255)也描述了执行这个重构的详细过程。

就像本文中的其他技术一样,对于何时执行从条件逻辑转换到多态类,没有硬性规定。事实上,如论在何种情况下我们都是不建议使用。在测试驱动的设计中:例如,Kent Beck设计了一个简单的货币系统,目的是使用多态类,但发现这使设计过于复杂,于是便将他的设计重新设计成一个非多态风格。经验和合理的判断将决定何时是将条件代码转换为多态代码的合适时间。

结束语

作为程序员,尽管平常所使用的常规技术可以解决大部分的问题,但有时我们应该打破这种常规,主动需求一些创新。毕竟作为一名开发人员,扩展自己知识面的的广度和深度,不仅能让我们做出更明智的决定,也能让我们变得越来越聪明。

相关推荐:

总结Java SE、Java EE、Java ME三者的区别

【java教程】Java String类

以上がJava コードを記述するための 4 つのヒントを要約するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。