Если вы теперь обязаны оптимизировать код Java, который вы пишете, что бы вы сделали? В этой статье автор вводит четыре метода, которые могут улучшить производительность системы и читаемость кода. Если вы заинтересованы в этом, давайте посмотрим.
Наши обычные задачи по программированию - не что иное, как применение одного и того же технического пакета к разным проектам. В большинстве случаев эти технологии могут соответствовать целям. Тем не менее, некоторые проекты могут потребовать особых методов, поэтому инженеры должны подробно учиться, чтобы найти самые простые, но наиболее эффективные методы. В предыдущей статье мы обсудили четыре специальных технологии, которые можно использовать при необходимости для создания лучшего программного обеспечения Java; В то время как в этой статье мы представим некоторые общие стратегии проектирования и методы реализации целей, которые помогают решить общие проблемы, а именно:
Только целенаправленная оптимизация
Используйте перечисления как можно больше для постоянных
Переопределить метод equals () в классе
Используйте как можно больше полиморфизма
Стоит отметить, что методы, описанные в этой статье, не применимы ко всем случаям. Кроме того, когда и где эти технологии должны использоваться, они требуют, чтобы пользователи тщательно рассмотрели.
1. только целенаправленная оптимизация
Крупные программные системы должны быть очень обеспокоены проблемами производительности. Хотя мы надеемся, что сможем написать наиболее эффективный код, много раз, если мы хотим оптимизировать код, мы понятия не имеем, как начать. Например, повлияет ли следующий код на производительность?
public void processIntegers (list <Integer> integers) {for (целое число: integers) {for (int i = integers.size ()-1; i> = 0; i--) {value += integers.get (i); }}}Это зависит от ситуации. В приведенном выше коде мы видим, что его алгоритм обработки составляет O (N ним) (используя большие символы O), где n - размер набора списков. Если n составляет всего 5, то не будет проблем, будет выполнено только 25 итераций. Но если n составляет 100 000, это может повлиять на производительность. Обратите внимание, что даже в этом случае мы не можем определить, что будут проблемы. Хотя этот метод требует 1 миллиарда логических итераций, будет ли он повлиять на производительность, еще предстоит обсудить.
Например, предположим, что клиент выполняет этот код в своем собственном потоке и асинхронно ждет для завершения расчета, то время его выполнения может быть приемлемым. Точно так же, если система развернута в производственной среде, но ни один клиент не вызывает ее, тогда нам нет необходимости оптимизировать этот код, поскольку она вообще не будет потреблять общую производительность системы. Фактически, система станет более сложной после оптимизации производительности, но трагическая вещь в том, что производительность системы не улучшается в результате.
Самое главное, что в мире нет бесплатного обеда, поэтому для снижения стоимости мы обычно используем такие технологии, как кеш, расширение петли или предварительно рассчитанные значения для достижения оптимизации, что, в свою очередь, увеличивает сложность системы и уменьшает читаемость кода. Если эта оптимизация может улучшить производительность системы, она того стоит, даже если она станет сложной, но прежде чем принять решение, вы должны сначала узнать эти две части информации:
Каковы требования к производительности
Где узкое место производительности
Прежде всего, нам нужно четко знать, каковы требования к производительности. Если в конечном итоге это соответствует требованиям, и конечный пользователь не поднял никаких возражений, то нет необходимости выполнять оптимизацию производительности. Однако, когда добавляются новые функции или объем данных системы достигает определенной шкалы, они должны быть оптимизированы, в противном случае могут возникнуть проблемы.
В этом случае это не должно основываться на интуиции или проверке. Потому что даже опытные разработчики, такие как Мартин Фаулер, склонны к созданию неверной оптимизации, как объяснено в рефакторинге статьи (стр. 70):
Если вы анализируете достаточно программ, вы найдете интересную вещь о производительности, которую большая часть вашего времени потрачена в небольшую часть кода в системе. Если все коды оптимизированы одинаково, конечным результатом является то, что 90% оптимизации потрачены, потому что код после оптимизации не работает много частоты. Время, потраченное на оптимизацию без целей, является пустой тратой времени.
Как разработчик с боем, мы должны серьезно относиться к этой точке зрения. Первое предположение заключается не только только в том, что производительность системы не была улучшена, но 90% времени разработки полностью потрачено впустую. Вместо этого мы должны выполнить общие варианты использования в производстве (или предварительном производстве) и выяснить, какая часть системы потребляет системы системы во время выполнения, а затем настроить систему. Например, только 10% кода, который потребляет большинство ресурсов, а затем оптимизация оставшихся 90% кода является пустой тратой времени.
Согласно результатам анализа, если мы хотим использовать эти знания, мы должны начать с наиболее распространенных ситуаций. Потому что это гарантирует, что фактические усилия в конечном итоге улучшат производительность системы. После каждой оптимизации шаги анализа должны быть повторены. Поскольку это не только гарантирует, что производительность системы действительно улучшается, также можно увидеть, какая часть узкого места производительности является после оптимизации системы (потому что после решения одного узкого места другие узкие места могут потреблять больше общих ресурсов системы). Следует отметить, что процент времени, проведенного в существующих узких местах, может увеличиться, так как оставшиеся узкие места временно неизменны, и общее время выполнения должно быть уменьшено, поскольку целевое узкое место устранено.
Хотя для полной проверки профилей в системах Java требуется много возможностей, существуют некоторые очень распространенные инструменты, которые могут помочь обнаружить горячие точки производительности системы, включая Jmeter, Appdynamics и Yourkit. Кроме того, вы также можете обратиться к Руководству Dzone по мониторингу эффективности для получения дополнительной информации об оптимизации производительности программы Java.
Хотя производительность является очень важным компонентом многих крупных программных систем и является частью автоматического тестового набора в конвейере продукта, его нельзя оптимизировать слепо и без цели. Вместо этого следует сделать конкретные оптимизации для узких мест производительности, которые были освоены. Это не только помогает нам избежать увеличения сложности системы, но и позволяет нам избегать обхода и избегать оптимизации, пробуждающих время.
2. Попробуйте использовать перечисления для постоянных
Существует много сценариев, в которых пользователи должны перечислить набор предопределенных или постоянных значений, таких как коды ответов HTTP, которые могут встречаться в веб -приложениях. Одним из наиболее распространенных методов реализации является создание нового класса, который содержит много статических значений конечного типа. Каждое значение должно иметь комментарий, описывающий, что означает значение:
открытый класс 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) {// Сделайте что -нибудь, если код ответа в порядке}Уже очень хорошо иметь эту идею, но есть еще некоторые недостатки:
Нет строгой проверки входящих целочисленных значений
Поскольку это основной тип данных, метод кода состояния не может быть вызван
В первом случае конкретная константа создается просто для представления специального целочисленного значения, но нет никаких ограничений на метод или переменную, поэтому используемое значение может быть за пределами области определения. Например:
открытый класс httpresponsehandler {public static void printmessage (int statuscode) {System.out.println ("Числовое статус" + statuscode); }} Httpresponshandler.printmessage (15000);Хотя 15000 не является действительным кодом ответа HTTP, на стороне сервера нет никаких ограничений, что клиент должен предоставить допустимые целые числа. Во втором случае у нас нет возможности определить метод кода состояния. Например, если вы хотите проверить, является ли заданный код состояния успешным кодом, вы должны определить отдельную функцию:
открытый класс httpresponsecodes {public static final int ok = 200; Public Static Final int not_found = 404; общественный статический финал int forbidden = 403; Public Static Boolean Issuccess (int statusCode) {return StatusCode> = 200 && StatusCode <300; }} if (httpresponsecodes.issuccess (gethttpresponse (). getStatuscode ())) {// Сделайте что -нибудь, если код ответа является кодом успеха}Чтобы решить эти проблемы, нам необходимо изменить постоянный тип из базового типа данных на пользовательский тип и разрешить только конкретные объекты пользовательского класса. Это именно то, для чего нужны Java Enums. Используя Enum, мы можем решить эти две проблемы одновременно:
public enum httpresponsecodes {ok (200), запрещен (403), not_found (404); частный окончательный код INT; Httpresponsecodes (int code) {this.code = code; } public int getCode () {return Code; } public boolean issuccess () {return code> = 200 && code <300; }} if (gethttpresponse (). getStatuscode (). issuccess ()) {// Сделать что -то, если код ответа является кодом успешного}Точно так же теперь можно потребовать, чтобы код состояния должен был быть действительным при вызове метода:
открытый класс httpresponshandler {public static void printmessage (httpresponsecode statuscode) {System.out.println ("Числовое состояние" + statuscode.getCode ()); }} Httpresponshandler.printmessage (httpresponsecode.ok);Стоит отметить, что этот пример показывает, что если он постоянный, вы должны попытаться использовать перечисления, но это не означает, что вы должны использовать перечисления при любых обстоятельствах. В некоторых случаях может быть желательно использовать постоянную для представления конкретного значения, но также разрешены другие значения. Например, каждый может знать о PI, и мы можем использовать постоянную для захвата этого значения (и повторно использовать его):
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 Dister; Public Class Run (Double Area) {this.area = area; } public double getCost () {return Area * 2; }} // Создать ковер диаметром 4 фута (радиус 2 фута) коврик fourfootrug = новый ковер (2 * NumericConstants.Unit_Circle_area);Следовательно, правила использования перечисления могут быть обобщены как:
Когда все возможные дискретные значения были известны заранее, вы можете использовать перечисление
Возьмите код ответа HTTP, упомянутый выше в качестве примера. Мы можем знать все значения кода состояния HTTP (можно найти в RFC 7231, который определяет протокол HTTP 1.1). Следовательно, перечисление используется. При расчете PI мы не знаем всех возможных значений о PI (любой возможный двойник действителен), но в то же время мы хотим создать постоянную для круговых ковров, чтобы упростить расчет (легче читать); Поэтому определяется серия констант.
Если вы не можете знать все возможные значения заранее, но хотите включить поля или методы для каждого значения, то самый простой способ - создать новый класс для представления данных. Хотя я никогда не говорил, что в каком -либо сценарии не должно быть никакого перечисления, ключом к тому, чтобы знать, где и когда не использовать перечисление, является заранее знать все значения и запретить использование любого другого значения.
3. Переосмыслить метод equals () в классе
Распознавание объекта может быть сложной проблемой для решения: если два объекта занимают одну и ту же позицию в памяти, они одинаковы? Если их идентификаторы одинаковы, они одинаковы? Или что, если все поля равны? Хотя у каждого класса есть своя собственная логика идентификации, в системе есть много западных стран, которые должны судить, являются ли они равными. Например, ниже класс, который указывает на покупку заказа ...
открытый класс покупка {Private Long ID; public long getId () {return id; } public void setId (long id) {this.id = id; }}... Как написано ниже, в коде должно быть много мест, которые похожи:
Купить OriginalPurchase = new Buick (); покупка upplytedPurchase = new Buick (); if (OriginalPurchase.getId () == updatedPurchase.getId ()) {// Выполнить некоторую логику для равных покупок}Чем больше эти логические вызовы (в свою очередь, это нарушает сухой принцип), покупка
Информация о личности класса также становится все больше и больше. Если по какой -то причине, покупка была изменена
Идентификационная логика класса (например, тип идентификатора был изменен), поэтому должно быть много мест, где обновляется логика идентификации.
Мы должны инициализировать эту логику внутри класса, а не слишком много распространять идентификационную логику класса покупки через систему. На первый взгляд, мы можем создать новый метод, такой как Issame, параметр включения которого является объектом покупки, и сравнить идентификаторы каждого объекта, чтобы увидеть, являются ли они одинаковыми:
открытый класс покупка {Private Long ID; Public Boolean Issame (купить другие) {return getId () == Другое. Gerid (); }}Хотя это эффективное решение, встроенная функциональность Java игнорируется: использование метода Equals. Каждый класс в Java наследует класс объектов, хотя он неявенный, поэтому он также наследует метод Equals. По умолчанию этот метод проверяет идентичность объекта (тот же объект в памяти), как показано в следующем фрагменте кода в определении класса объекта (версия 1.8.0_131) в JDK:
Public Boolean Equals (Object obj) {return (this == obj);}Этот метод равняется естественному месту для инъекции идентификационной логики (реализовано переоценкой по умолчанию равна):
открытый класс покупка {Private Long ID; public long getId () {return id; } public void setId (long id) {this.id = id; } @Override public boolean equals (объект другой) {if (this == Другое) {return true; } else if (! (Другой экземпляр покупки)) {return false; } else {return (((покупка) другого) .getId () == getId (); }}}Хотя этот метод равняется сложным, поскольку метод Equals принимает только параметры объектов типа, нам нужно рассмотреть только три случая:
Другим объектом является текущий объект (то есть оригинал purchase.equals (OriginalPurchase)), по определению, они являются тем же объектом, поэтому верните True
Другой объект не является объектом покупки, в этом случае мы не можем сравнить идентификатор покупки, поэтому два объекта не равны
Другие объекты - это не один и тот же объект, но это случаи покупки. Следовательно, равен ли равенство от того, равны ли текущий идентификатор покупки и другая покупка. Теперь мы можем рефакторировать наши предыдущие условия следующим образом:
Купить OriginalPurchase = new Buick (); покупка upplytedPurchase = new Buick (); if (OriginalPurchase.equals (updatedPurchase)) {// Выполнить некоторую логику для равных покупок}В дополнение к сокращению репликации в системе, рефакторирование метода по умолчанию Equals имеет некоторые другие преимущества. Например, если мы построим список объектов покупки и проверим, содержит ли список другой объект покупки с одним и тем же идентификатором (разные объекты в памяти), то мы получаем истинное значение, поскольку два значения считаются равными:
List <shipe> покупки = new ArrayList <> (); покупка.адд (OriginalPurchase); покупки. // Истинный
Обычно, где бы вы ни находились, если вам нужно определить, равны ли два класса, вам нужно только использовать метод переписываемых равных. Если мы хотим использовать метод равных неявно из -за наследства объекта объекта для оценки равенства, мы также можем использовать оператор == следующим образом:
if (OriginalPurchase == updatedPurchase) {// Два объекта одинаковы в памяти}Следует также отметить, что после перезаписываемого метода equals метод хэшкода также должен быть переписан. Больше информации о взаимосвязи между этими двумя методами и о том, как правильно определить хэшкод
Метод, см. Этот поток.
Как мы видели, перезапись метода Equals не только инициализирует идентификационную логику внутри класса, но и уменьшает распространение этой логики по всей системе, но и позволяет языку Java принимать хорошо информированные решения о классе.
4. Используйте полиморфизмы как можно больше
Для любого языка программирования условные предложения являются очень распространенной структурой, и есть определенные причины их существования. Потому что различные комбинации могут позволить пользователю изменить поведение системы на основе заданного значения или мгновенного состояния объекта. Предполагая, что пользователю необходимо рассчитать баланс каждого банковского счета, можно разработать следующий код:
public enum bankaccounttype {Проверка, сбережения, сертификат_оф_ДПОЗИТ;} открытый класс Bankaccount {Private Final Final Bankaccounttype Тип; public bankaccount (bankaccounttype type) {this.type = type; } public double getInterestrate () {switch (type) {checking: return 0.03; // 3% экономия случая: возврат 0,04; // 4% сертификат случая_оф_DEPOSIT: возврат 0,05; // 5% по умолчанию: бросить новый UnsupportedOperationException (); }} public boolean supportsdeposits () {switch (type) {case exking: return true; Сэкономить случай: вернуть истину; case sertiate_of_deposit: вернуть false; по умолчанию: добавьте новое UnsupportedOperationException (); }}}Хотя приведенный выше код соответствует основным требованиям, существует очевидный недостаток: пользователь только определяет поведение системы на основе типа данной учетной записи. Это не только требует, чтобы пользователи проверяли тип учетной записи, прежде чем принимать решение, но также необходимо повторить эту логику при принятии решения. Например, в приведенной выше дизайне пользователь должен проверить оба метода. Это может привести к выходу из -под контроля, особенно при получении необходимости добавить новый тип учетной записи.
Мы можем использовать полиморфизм для неявного принятия решений, а не использовать типы учетных записей для их различения. Для этого мы преобразуем конкретные классы BankAccount в интерфейс и передаем процесс принятия решения в серию конкретных классов, которые представляют каждый тип банковского счета:
/*** Java Learning and Communication QQ Группа: 589809992 Давайте вместе изучим Java! */public interface bankaccount {public double getInterestrate (); Общедоступные логические поддержанные депозиты ();} публичный класс CheckingAccount реализует Bankaccount {@Override public getIntestrate () {return 0.03; } @Override public Boolean Support Deposits () {return true; }} public Class SavingsAccount реализует BankAccount {@Override public double getIntestrate () {return 0.04; } @Override public boolean supportsdeposis () {return true; }} Общедоступный сертификат класса. } @Override public boolean supportdeposis () {return false; }}Это не только инкапсулирует информацию, конкретную для каждой учетной записи, в свой собственный класс, но также поддерживает пользователей изменять свои проекты двумя важными способами. Во -первых, если вы хотите добавить новый тип банковского счета, вам просто нужно создать новый конкретный класс, внедрить интерфейс Bankaccount и дать конкретную реализацию двух методов. В конструкции условной структуры мы должны добавить новое значение в перечисление, добавить новый оператор по делу обоих методов и вставить логику новой учетной записи в соответствии с каждой оператором дела.
Во -вторых, если мы хотим добавить новый метод в интерфейс Bankaccount, нам просто нужно добавить новый метод в каждом бетонном классе. В условном дизайне мы должны скопировать существующий оператор Switch и добавить его в наш новый метод. Кроме того, мы должны добавить логику для каждого типа учетной записи в каждом операторе дела.
Математически, когда мы создаем новый метод или добавляем новый тип, мы должны внести такое же количество логических изменений в полиморфном и условном дизайне. Например, если мы добавим новый метод в полиморфную конструкцию, мы должны добавить новый метод в конкретные классы всех счетов N Bank, а в условном дизайне мы должны добавить N Новые операторы в нашем новом методе. Если мы добавим новый тип учетной записи в полиморфную конструкцию, мы должны реализовать все M -номера в интерфейсе Bankaccount, а в условном дизайне мы должны добавить новый оператор для каждого существующего метода M.
Хотя количество изменений, которые мы должны внести, равно, природа изменений совершенно отличается. В полиморфическом дизайне, если мы добавим новый тип учетной записи и забываем включить метод, компилятор бросает ошибку, потому что мы не реализуем все методы в нашем интерфейсе Bankaccount. В условном дизайне нет такой проверки, чтобы убедиться, что у каждого типа есть оператор дела. Если добавлен новый тип, мы можем просто забыть обновить каждый оператор Switch. Чем серьезнее эта проблема, тем больше мы повторяем наш оператор Switch. Мы люди, и мы склонны делать ошибки. Таким образом, каждый раз, когда мы можем полагаться на компилятор, чтобы напомнить нам об ошибках, мы должны сделать это.
Второе важное примечание об этих двух конструкциях заключается в том, что они эквивалентны внешним. Например, если мы хотим проверить процентную ставку для текущего счета, условная конструкция будет выглядеть следующим образом:
Bankaccount checkingaccount = new Bankaccount (bankaccounttype.checking); System.out.println (ceckingAccount.getInterestrate ()); // Выход: 0,03
Вместо этого полиморфные конструкции будут похожи на следующее:
Bankaccount checkingAccount = new CeckingAccount (); System.out.println (ceckingAccount.getInterestrate ()); // Выход: 0,03
С внешней точки зрения мы просто звонят в getIntereunk () на объекте Bankaccount. Это будет еще более очевидно, если мы абстрагируем процесс создания в фабричный класс:
Public Class ConditionalAccountFactory {public static bankaccount createCheckCount () {return new bankaccount (bankaccounttype.checking); }} public class polymorphicaccountfactory {public static bankaccount createCheckingAccount () {return new CeckingAccount (); }} // В обоих случаях мы создаем учетные записи с использованием FactoryBankAccount CondityAlcheckCount = condityalAccountFactory.createCheCkCount (); BankAccount polymorphicChyckingAccount = polymorphicAccountfactory.crateChackCcount ();// в обоих примерах samesystem.out.println (ConditeLcheckingAccount.getInterestRate ()); // Выход: 0.03System.out.println (Polymorphiccheckingaccount.getInterestrate ()); // Выход: 0,03Очень часто заменяет условную логику полиморфными классами, поэтому были опубликованы методы для восстановления условных операторов в полиморфные классы. Вот простой пример. Кроме того, рефакторинг Мартина Фаулера (стр. 255) также описывает подробный процесс выполнения этой реконструкции.
Как и другие методы в этой статье, не существует жесткого и быстрого правила о том, когда выполнить переход от условной логики к полиморфным классам. На самом деле, мы не рекомендуем использовать его в любой ситуации. В дизайне, основанном на тестировании: например, Кент Бек разработал простую валютную систему с целью использования полиморфных классов, но обнаружил, что это сделало дизайн слишком сложным и перепроектировал его дизайн в неполиморфический стиль. Опыт и разумное суждение определят, когда подходящее время для преобразования условного кода в полиморфический код.
Заключение
Как программисты, хотя обычные методы, используемые в обычное время, могут решать большинство проблем, иногда мы должны нарушать эту рутину и активно требовать некоторых инноваций. В конце концов, как разработчик, расширение широты и глубины его знаний не только позволяет нам принимать более умные решения, но и делает нас умнее.