이 기사는 Java 코드 작성을 위한 네 가지 기술을 요약한 것입니다. 이는 특정 참고 가치가 있습니다. 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.
우리의 일반적인 프로그래밍 작업은 동일한 기술 제품군을 다른 프로젝트에 적용하는 것 이상입니다. 대부분의 경우 이러한 기술은 목표를 달성할 수 있습니다. 그러나 일부 프로젝트에는 특수 기술이 필요할 수 있으므로 엔지니어는 가장 간단하면서도 효과적인 방법을 찾기 위해 더 깊이 탐구해야 합니다. 이 기사에서는 일반적인 문제를 해결하는 데 도움이 될 수 있는 몇 가지 일반적인 디자인 전략과 목표 구현 기술을 소개합니다. 클래스 equals() 메소드
다형성을 최대한 사용하세요
이 기사에서 설명하는 기술이 모든 상황에 적합하지는 않다는 점은 주목할 가치가 있습니다. 또한 이러한 기술을 언제, 어디서 사용해야 하는지에 대한 사용자의 세심한 고려가 필요합니다.
대형 소프트웨어 시스템은 성능 문제를 매우 염려해야 합니다. 가장 효율적인 코드를 작성하고 싶지만 코드를 최적화하고 싶다면 시작할 방법이 없는 경우가 많습니다. 예를 들어 다음 코드가 성능에 영향을 미칠까요?
public void processIntegers(List<Integer> integers) { for (Integer value: integers) { for (int i = integers.size() - 1; i >= 0; i--) { value += integers.get(i); } } }
예를 들어, 클라이언트가 자체 스레드에서 이 코드를 실행하고 계산이 완료될 때까지 비동기적으로 기다린다고 가정하면 실행 시간은 허용될 수 있습니다. 마찬가지로 시스템이 프로덕션 환경에 배포되었지만 호출할 클라이언트가 없는 경우 시스템의 전체 성능을 전혀 소모하지 않기 때문에 이 코드를 전혀 최적화할 필요가 없습니다. 실제로 성능을 최적화하면 시스템이 더욱 복잡해지겠지만, 결과적으로 시스템 성능이 향상되지 않는다는 비극이 있습니다.
성능 병목 현상은 어디에 있습니까?
먼저 성능 요구 사항이 무엇인지 명확하게 알아야 합니다. 궁극적으로 요구 사항에 부합하고 최종 사용자가 어떠한 이의도 제기하지 않는다면 성능 최적화를 수행할 필요가 없습니다. 그러나 새로운 기능이 추가되거나 시스템의 데이터량이 일정 규모에 도달하면 최적화를 수행해야 하며, 그렇지 않으면 문제가 발생할 수 있습니다.
충분한 프로그램을 분석하면 다음과 같은 사실을 발견할 수 있습니다. 성능에 대한 흥미로운 점은 대부분의 경우 시스템의 작은 부분에서 낭비됩니다. 모든 코드에 대해 동일한 최적화가 수행되면 최적화된 코드가 자주 실행되지 않기 때문에 최종 결과는 최적화의 90%가 낭비되는 것입니다. 목표 없이 최적화에 소비하는 시간은 시간 낭비입니다.
분석 결과에 따르면 이 지식을 활용하려면 가장 일반적인 상황부터 시작해야 합니다. 이는 실제 노력이 궁극적으로 시스템 성능을 향상시키는 것을 보장하기 때문입니다. 각 최적화 후에는 분석 단계를 반복해야 합니다. 이는 시스템의 성능이 실제로 향상된다는 것을 보장할 뿐만 아니라 시스템을 최적화한 후 성능 병목 현상이 어디에 있는지도 보여주기 때문입니다(병목 현상 하나가 해결된 후에는 다른 병목 현상이 시스템의 전체 리소스를 더 많이 소비할 수 있기 때문입니다). 남은 병목 현상은 일시적으로 변하지 않기 때문에 기존 병목 현상에 소요되는 시간의 비율은 증가할 가능성이 높으며, 대상 병목 현상이 제거되면 전체 실행 시간은 감소해야 합니다.
尽管在Java系统中想要对概要文件进行全面检查需要很大的容量,但是还是有一些很常见的工具可以帮助发现系统的性能热点,这些工具包括JMeter、AppDynamics和YourKit。另外,还可以参见DZone的性能监测指南,获取更多关于Java程序性能优化的信息。
虽然性能是许多大型软件系统一个非常重要的组成部分,也成为产品交付管道中自动化测试套件的一部分,但是还是不能够盲目的且没有目的的进行优化。相反,应该对已经掌握的性能瓶颈进行特定的优化。这不仅可以帮助我们避免增加了系统的复杂性,而且还让我们少走弯路,不去做那些浪费时间的优化。
需要用户列出一组预定义或常量值的场景有很多,例如在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创建一个常量,使计算更容易(更容易阅读);因此定义了一系列常量。
如果不能提前知道所有可能的值,但是又希望包含每个值的字段或方法,那么最简单的方法就是可以新建一个类来表示数据。尽管没有说过什么场景应该绝对不用枚举,但要想知道在什么地方、什么时间不使用枚举的关键是提前意识到所有的值,并且禁止使用其他任何值。
对象识别可能是一个很难解决的问题:如果两个对象在内存中占据相同的位置,那么它们是相同的吗?如果它们的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语言对类做出有根据的决定。
对于任何一门编程语言来说,条件句都是一种很常见的结构,而且它的存在也是有一定原因的。因为不同的组合可以允许用户根据给定值或对象的瞬时状态改变系统的行为。假设用户需要计算各银行账户的余额,那么就可以开发出以下的代码:
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 코드 작성을 위한 네 가지 팁 요약의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!