Примечание редактора . Находясь на канале Java, большинство из нас очень хорошо знают язык и находятся в его экосистеме по крайней мере пару лет. Это дает нам рутину и опыт, но также вызывает определенное видение туннеля. В новой серии Outside-In Java не-Javaists расскажут нам о нашей экосистеме.
Я не имею дело с Java, поэтому я исследую, насколько верны все мои предвзятые представления об этом. В прошлый раз я в основном изучал проблемы пользователей, такие как скорость и размер. Результаты: неокончательные.
Но современная Java все чаще используется в местах, где она невидима для пользователей: в центрах обработки данных, на телефонах, на моем тостере. Так что, возможно, действительно интересные вопросы о том, как Java выглядит для разработчиков.
Java небезопасна
Глупость Java как ходячая проблема безопасности восходит к древним временам, когда Java-апплеты были обычным делом, вы верили, что JVM может эффективно изолировать их, а иногда и не может.
Может быть, попытка свести на нет весь язык общего назначения и его массивную стандартную библиотеку, чтобы быть достаточно безопасной для запуска из Интернета, была плохой идеей, но сейчас это спорный вопрос. Я очень редко вижу Java-апплеты. Я не думаю, что у меня даже установлен плагин NPAPI. Firefox не позволяет Java-апплетам запускаться автоматически , и полностью прекращает их поддержку в марте; Chrome потерял поддержку в прошлом году .
Конечно, это было отчасти потому, что Java-апплеты стали скорее поверхностью атаки, чем полезной платформой; В отчете CISCO за 2014 год утверждается, что 91% веб-эксплойтов были направлены на Java. Я думаю, что в том же году мой тогдашний работодатель предупреждал всех, чтобы они вручную отключали Java в своих браузерах, если им это не требовалось. Если это единственное знакомство с Java, то это не произведет большого впечатления.
Эй, подожди. Это должно быть с точки зрения разработчика. Так что насчет самой среды выполнения, независимо от проблем апплета? «Безопасный» трудно определить количественно, но в качестве грубого приближения я могу посмотреть количество CVE, выпущенных в этом году.
Э-э-э, это застало меня врасплох. Честно говоря, я ожидал, что буду приятно удивлен и явно не прав, но этот список делает звучание Java несколько хуже, чем я думал. Я надеюсь, что этому есть отличное объяснение, но у меня его нет.
Java — это предприятие
Ах, еще одно слово, которое все используют (включая меня), но это ничего не значит. Это создает очень специфическое изображение, но очень нечеткое определение. У меня есть несколько предположений относительно того, что это может означать.
Ява отвлечена в стратосферу
Абстрактосфера, если хотите. Царство печально известного AbstractSingletonProxyFactoryBean
.
Я немного запутался в этом. Снова обращаясь кasticsearch, я наткнулся на этот класс, WhitespaceTokenizerFactory
. Весь его исходный код:
public class WhitespaceTokenizerFactory extends AbstractTokenizerFactory {
public WhitespaceTokenizerFactory(
IndexSettings indexSettings,
Environment environment,
String name,
Settings settings) {
super(indexSettings, name, settings);
}
@Override
public Tokenizer create() {
return new WhitespaceTokenizer();
}
}
Да конечно. Вы хотите иметь возможность создавать произвольный токенизатор из некоторого внешнего состояния, но не хотите, чтобы сами токенизаторы зависели от внешнего состояния. Имеет смысл.
Тем не менее, этот код выглядит довольно глупо, особенно если вы не видели других классов, которые делают более сложные вещи. Одни и те же слова повторяются три раза; 38-строчный файл содержит только две строки кода. Это легко посмотреть и подумать, что Java-код доходит до нелепых крайностей со своей косвенностью. В худшем случае я мог бы сделать это в Python:
@builder_for(WhitespaceTokenizer)
def build(cls, index_settings, env, name, settings):
return cls()
@builder_for(SomeOtherTokenizer)
def build(cls, index_settings, env, name, settings):
return cls(index_settings.very_important_setting)
# etc.
Я машу рукой, как это на самом деле работает, но в этом нет ничего особенного. Можно даже подумать об этом на Java, но, вероятно, это не красиво и идиоматично. В качестве альтернативы, код Python может просто иметь build
Одна приятная вещь о динамической типизации состоит в том, что код может использовать тип независимо от него. Класс tokenizer может работать с IndexSettings
Environment
Это немного сомнительно, но в таком случае, когда все внутреннее, это может иметь смысл.
Но, учитывая, что система типов Java является тем, чем она является, я могу понять, почему вы в конечном итоге получили приведенный выше код. Что меня смущает, так это
Почему я не вижу то же самое на других языках?
Я обнаружил эту коллекцию крошечных фабричных классов примерно через минуту случайного поиска в самом популярном Java-проекте на GitHub. Я совершенно не удивлен этим. И все же я не могу вспомнить, чтобы что-то подобное было в других явных статически типизированных языках. Где крошечные фабричные классы в C ++? Самый популярный проект C ++ — это Electron , и поиск «фабрики» только находит мне код, подобный этому , который имеет гораздо больше возможностей. Самым популярным проектом Objective-C является AFNetworking , который содержит «фабрику» один раз — в журнале изменений. Самым популярным проектом Swift является Alamofire , который так или иначе не содержит слова «фабрика»!
Поэтому, хотя я могу согласиться с тем, что уровни косвенности и крошечные классы полезны для взаимодействия с системой типов в стиле C ++, я не понимаю, почему я вижу их гораздо чаще в Java, чем даже в C ++.
Это культурная разница? Довольны ли разработчики C ++ запутанной паутиной взаимосвязанных зависимостей? Существуют ли эти крошечные классы в C ++, но живут ли они вместе в одном файле, где их гораздо легче игнорировать?
Ясно, что Java живет в абстрактной сфере, но я не могу понять, почему она так отличается от похожих языков.
Ява утомительно многословна
«Предприятие» заставляет меня думать о повторяющейся бюрократии, высасывающей радость из всего.
Аксессоры везде
И Java заставляет меня думать о средствах доступа. Та же идея, правда.
private int foo;
public int getFoo() {
return this.foo;
}
public setFoo(int foo) {
this.foo = foo;
}
Посмотрите на весь этот код, поглощающий драгоценное вертикальное пространство, чтобы ничего не делать. Я мог бы просто сказать public int foo;
и было сделано с этим.
В мире есть три типа программистов, отличающихся тем, как они реагировали на последний абзац. Некоторые кивнули головой, и, вероятно, они программисты на Python. Некоторые возразили, что это нарушает инкапсуляцию, и снова откажутся, когда я скажу, что меня не волнует инкапсуляция. Наконец, некоторые закатили глаза и отметили, что public
Ах, эти последние люди могут иметь смысл. Проблема в том, что Java не поддерживает свойства. «Свойство» — это ужасное общее название для языковой функции, которая стала популярной лишь в последнее время, но если вы не знакомы, я имею в виду эту волшебную вещь, которую вы можете сделать в Python. Если у вас есть атрибут foo
class Bar:
def __init__(self):
# Leading underscore is convention for "you break it, you bought it"
self._foo = 3
@property
def foo(self):
return self._foo
@foo.setter
def foo(self, foo):
if foo % 2 == 0:
raise ValueError("foo must be odd")
self._foo = foo
bar = Bar()
bar.foo = 8 # ValueError: foo must be odd
@property
Другой код все еще может работать с obj.foo
Даже сам @property
Я знаю, что Python, Swift и ряд языков поддержки .NET (C #, F #, VB, Boo, …) поддерживают свойства. Предполагается, что JavaScript поддерживает их в настоящее время, хотя я не уверен, насколько сильно на них опирается код. В Ruby они есть, с немного другой семантикой. Lua и PHP могут подделать их. У Perl есть вещь, но вы, вероятно, не должны ее использовать. Сама JVM должна поддерживать их, поскольку существуют Jython и JRuby. Так почему бы не Java язык?
Мне кажется странным, что Java не воспользовалась этой возможностью, которая исключала бы много повторений. Это было очевидно предложено для Java 7, но я не могу найти объяснение, почему это не сделало сокращение, и теперь это, кажется, очень не приоритет .
Но подождите, есть больше
Здесь я показываю свои цвета Python, но пока я в этом: Еще одна хитрость в том, что классами легко манипулировать во время загрузки модуля. Определение класса — это просто код, который создает класс при выполнении. Итак, в Python есть несколько интересных махинаций, таких как модуль attrs , который позволяет делать это:
import attr
@attr.s
class Point:
x = attr.ib(default=0)
y = attr.ib(default=0)
С атрибутами, объявленными так, вы получаете бесплатно: конструктор, который принимает аргументы по порядку или по имени; разумный repr
toString
hashability; операторы сравнения; и согласие на неизменность. Нет генерации кода, просто некоторые быстрые манипуляции с классом, как он определен во время выполнения.
Очевидно, что этот точный подход не будет работать в Java, но его можно смоделировать. Я знаю, что Java IDE почти печально известны своим объемом генерации кода, поэтому я немного удивлен, что сама Java не использовала способ генерировать или переписывать код во время компиляции.
Не повторяй себя
Аналогичным образом, это, похоже, распространенная проблема:
ComicallyLongStrawmanTypeName value = new ComicallyLongStrawmanTypeName();
Небольшой вывод типа будет иметь большое значение здесь. В последнее время Java получила некоторый вывод типов, но только для обобщений:
List<ComicallyLongStrawmanTypeName> value = new ArrayList<>();
Определенно улучшение, но я немного озадачен тем, почему эта функция остановилась здесь. На самом деле это обратный подход к тому, как я ожидаю, что вывод типов будет работать — обычно первым шагом является вывод типа переменной из типа выражения, а не наоборот. Я видел некоторые предположения о более традиционном выводе типов в Java 10, и если это удастся, будет неплохое улучшение.
ComicallyLongStrawmanTypeName
Также стоит упомянуть само ComicallyLongStrawmanTypeName
Java печально известна своими очень длинными именами типов. Я никогда даже не думал об этом до сих пор, но это почти наверняка вызвано … дизайном системы пакетов!
Имена пакетов, как правило, начинаются, по крайней мере, с двухкомпонентного домена и имени проекта, например org.mozilla.rhino
Проблема в том, что имена пакетов и классов не могут быть псевдонимами. Пакеты также не являются иерархическими, поэтому вы не можете импортировать целую «ветвь» пакета. Если у вас есть пакет, содержащий класс, вы можете обратиться к нему ровно двумя способами: как org.mozilla.rhino.ClassName
ClassName
Вот и все.
В результате имена классов должны быть несколько квалифицированными, чтобы избежать конфликтов имен! Если вы назовете класс List
List
Таким образом, вы получите пакет com.bar.foo
FooBarList
Это кажется немного противоположным пункту упаковки. В Python я могу создать псевдоним пакета, или импортировать родительский пакет, или псевдоним имени класса. Я могу заполнить файл классами с очень общими именами, затем импортировать этот файл с коротким псевдонимом и использовать его содержимое как pkg.List
Это здорово и серьезно сокращает повторный шум. Но в Java вы должны называть классы так, как если бы они были частью единого глобального пространства имен, потому что они могут быть импортированы вместе с любым другим классом.
(Между прочим, этот вид тонкого глобального пространства имен также заставляет меня опасаться интерфейсов в стиле Java. Имена методов в интерфейсе эффективно совместно используют единое глобальное пространство имен со всеми именами методов во всем коде Java везде — потому что вся точка интерфейсов что класс может реализовать любой произвольный набор из них. Таким образом, вы также не можете использовать хорошие короткие имена здесь, или вы рискуете конфликтом именования. Это не удар по Java, хотя — та же проблема существует в во многих языках, включая Python, хотя отсутствие явных интерфейсов делает его менее значимым. Rust в основном избегает этого, делая отдельные реализации интерфейсов, а не частью единого тела класса. Я считаю, что идея пришла из семейства ML.)
Что-то, что выделяется для меня, это то, что Java позволяет вам быть немного кратким: неявное this
Я не очень люблю this
Это делает сканирование файла очень трудным, потому что, когда я вижу это:
foo = 3;
Я не могу сразу определить, является ли это локальной переменной или атрибутом, не проверив остальную часть метода. (Да, да, IDE, но актуальная тема здесь заключается в том, что я думаю, что язык должен использоваться без него.) Я уверен, что многие люди не согласны со мной здесь, и это нормально; Я просто говорю, что неявное this
Я немного удивлен; Я думал, что многословность Java будет скорее культурным феноменом, чем свойством языка, но, похоже, сама Java непреднамеренно продвигает гибкий код. Некоторые недавние языковые изменения выглядят многообещающе, и я надеюсь увидеть больше работы над этим в будущем.
Краткая касательная
На этом этапе я начинаю понимать, что большая часть многословия Java на самом деле связана со стабильностью API :
-
public
private
- Принадлежности обеспечивают будущее интерфейс и дают вам возможность добавить больше логики позже.
- Статические типы делают интерфейс максимально консервативным.
- Интерфейсы и другие формы косвенного обращения сводят к минимуму функциональность, на которую опирается ваш код.
- Наследование также является частью API каждого класса, в который входит
protected
final class
- Заводы на будущее, чтобы потом не менять тип возвращаемого значения, чего нельзя сделать с конструкторами.
- Импорт действительно жесткий, потому что … э-э … ну, не могу объяснить это.
Но с точки зрения стабильности некоторые вещи в Java кажутся мне необычными.
Если гарантия, которую все на самом деле хотят, — это стабильный API, почему основной язык и инструментарий не имеют какого-либо способа … обеспечить стабильный API? Вместо этого у нас есть набор косвенных инструментов, таких как public
private
Если мы случайно сделаем серьезное изменение, нам лучше надеяться, что у нас будет тест, основанный на этом API, или мы никогда этого не заметим. Почему компьютер не может проверить эту гарантию для нас? Почему это не встроено ни в один из основных языков, когда так много из них предлагают предложить эту горстку примитивов?
Кроме того, в Java нет способа указать, какие части вашего API предназначены для публичного использования. Метод может быть частным или общедоступным; класс может быть частным или публичным; но я не знаю, как сказать, является ли весь файл (или каталог) частью общедоступного API или просто внутренней утилитой. «Приватный пакет» — это вещь, но поскольку пакеты не являются иерархическими, они заходят так далеко, если вы не хотите втиснуть весь ваш проект в один пакет. (Единственный язык, который мне известен, который хорошо справляется с этим, опять же, Rust.) Я бы предположил, что обходной путь — это сохранить отдельный набор классов публичной обертки, но если ваш публичный интерфейс окажется совершенно отдельным, то какой смысл все ключевые слова стабильности разбросаны по всему внутреннему коду? Возможно, модули Java 9 улучшат это.
Если подумать, интересно: много ли Java-разработчиков отказываются от стабильности для внутреннего кода? Я полагаю, что использование private
public
Я много раз слышал, что в Java IDE есть возможности рефакторинга, граничащие с волшебством, поэтому, безусловно, атрибут может быть автоматически изменен, чтобы использовать аксессоры при необходимости. Кто-нибудь воспользуется этим, чтобы избежать устойчи- вости будущего?
Я подозреваю, что ответ «нет», потому что методы доступа рассматриваются как лучшая практика — или даже по своей сути добродетельная, часть самой структуры ОО-дизайна. Если это так, то это удивительный и резкий контраст с Python, где средства доступа, как правило, рассматриваются как излишняя затея, потому что язык предоставляет способ изменять простые атрибуты, не нарушая API.
Аналогичным образом, Python вообще не имеет понятия «конфиденциальность». По соглашению, имя метода или атрибута, начинающееся с одного подчеркивания, является «частным», но это означает только «не вините меня, если это изменится позже» и практически не имеет значения для основного языка. Это очень полезно, когда сторонняя библиотека почти делает то, что я хочу, но где-то нет нужного мне хука. Я могу просто создать подкласс и обернуть «приватный» метод, если я готов принять небольшую хрупкость. Я могу случайно нарушить гарантии некоторых объектов, но знаю, что это моя собственная глупая ошибка. Помогает, что оригинальный исходный код, как правило, доступен.
Тем не менее, я видел, как люди настаивали на том, что Python не имеет «реальной» или «полной» поддержки ОО, просто потому, что в нем отсутствуют эти функции Java. Как будто «хороший дизайн / поддержка ОО» был эквивалентен «всему, что делает Java». Это какая-то мощная впечатляющая сила брендинга.
Неудобно, что принципы проектирования будут настолько радикально противоположны между двумя языками, и это предполагает, что, возможно, эти принципы не так принципиальны, как мы думаем. Предполагается, что инкапсуляция хороша, потому что она скрывает представление объекта, но иногда объект является его представлением, и у Java нет удовлетворительного способа выразить это. Имя хоста не является частью того, что делает URL; это часть того, что URL. Выражение «имя хоста этого URL» в форме вопроса или команды (то есть вызова метода) кажется мне неестественным и неловким. Но в Java у вас нет выбора, и в результате я столкнулся с разработчиками Java, которые рассматривают инкапсуляцию как абсолютную пользу — как будто принципы ОО были определены в терминах собственных ограничений Java.
Я не пытаюсь тряпкой на Java или разработчиков Java здесь. Я уверен, что взял много более экзотических принципов из написания такого большого количества Python. («Перегрузка оператора подразделения для объединения сегментов пути — это великолепный дизайн!») Но я восхищен тем, как наш собственный культурный контекст определяет то, как мы воспринимаем мир… и то, как мы решаем, каким должен быть мир.
Хорошо, где я был?
Ява чрезвычайно консервативна
Ах, теперь мы можем куда-нибудь добраться.
В конце концов, язык изменился сравнительно мало за эти годы. В 2004 году он подобрал дженерики, аннотации и некоторые другие тонкости, а в 2014 году — лямбда-выражения, но ничего более фундаментального. В отличие от этого, даже в C ++ в последние годы произошли значительные изменения.
Мне нравятся новые и интересные разработки в языках, но я понимаю привлекательность медленного и устойчивого подхода. За все время существования C было выпущено только три с половиной релиза: C89, C99, C11 и, возможно, C95. Язык достаточно мал и стабилен, так что передовые компиляторы по-прежнему должны обрабатывать код с 1990 года. Интерфейсы достаточно просты, чтобы скомпилированный код моего возраста имел хорошие шансы на соединение с новыми библиотеками. Это впечатляющий подвиг.
Сложность в том, что С не делает для вас много тяжелой работы, поэтому много работы переизобретается, и многое перерабатывается. C также редко удаляет или даже осуждает функции; известные минные поля в C помечены специальной коллекцией предупреждений компилятора.
Python, с другой стороны, выпустил четыре релиза только за последние пять лет. Устаревшие функции или библиотеки иногда устаревают и удаляются несколькими версиями позже, поэтому код Python может нуждаться в небольшом текущем обслуживании, если он хочет работать с будущими выпусками Python. Python также не имеет стабильного скомпилированного формата, чтобы изолировать развернутую библиотеку от языковых изменений. Сам язык задан достаточно хорошо, что существует несколько альтернативных реализаций, но они, как правило, отстают от нескольких выпусков.
Похоже, Java стремится к интересному компромиссу. Базовый язык растет относительно медленно, что устраняет необходимость в дальнейшем устранении ошибок. Стандартная библиотека довольно обширна, но если в этом списке устаревших данных есть какие-либо указания, старые API редко удаляются напрямую — я вижу здесь функции, которые устарели в Java 1.3 шестнадцать лет назад.
Это предполагает сильный акцент на стабильности и совместимости. Новые функции добавляются очень аккуратно, особенно в основной язык. Чтобы компенсировать это, экосистема создала множество инструментов поверх Java; сторонний код может экспериментировать более свободно, и консервативные разработчики могут просто не использовать такие инструменты. Я заметил, что Java-аннотации изначально были хакерской копией, поэтому кажется, что основной язык принимает популярные идеи из экосистемы, и это хорошо.
Недостатком является то, что Java имеет определенную репутацию для генерации кода, уровней конфигурации / отражения для выполнения задач, которые затрудняет основной язык, и множества XML по какой-то причине.
С другой стороны, поддержание стабильного роста языка, вероятно, облегчило эксперименты с JVM, что, в свою очередь, открыло двери для нескольких новых языков. Clojure, Scala и Groovy могут не существовать без солидной виртуальной машины и обширной экосистемы используемых библиотек.
Возможно, «Ява консервативна» слишком проста. Точнее, кажется, что Java очень осторожно обосновал свою либеральность. В конце концов, консерватизм — это, по сути, неприятие риска, и это определенно черта, связанная с миром бизнеса.
просветление
По этой извилистой дороге я понял кое-что еще. Многие мои специфические неприязни к базовому языку на самом деле являются ошибкой C ++.
Получатели, установщики и никакие свойства … точно так же как C ++.
Контроль доступа … так же, как C ++. Возможно, немного более оправдано в C ++, где вмешательство во внутренние объекты может привести к повреждению памяти.
Один класс на файл … так же, как (ну, много) C ++.
Скобки, точки с запятой и dromedaryCase… точно так же, как C ++. Это отдельная банка червей, но я думаю, что брекеты — это шум.
Повторение имени класса для конструкторов … так же, как C ++. Вероятно, было бы хорошо написать public new()
Возможность использовать типы без явного их импорта … как в C ++. Хорошо, да, Java намного лучше в этом, но скимминга для import
Нулевое значение … так же, как C ++. Это особенно усугубляет, поскольку делает всю систему типов бессмысленной. Переменная типа T
T
или может содержать значение null
T
Каждая отдельная аннотация является ложью. Даже Optional<T>
null
И наконец
У Явы есть несколько бородавок. Языковой дизайн накладывает некоторые утомительные накладные расходы на разработку; проблемы безопасности в самой платформе, возможно, слишком распространены; и медленно меняется, к лучшему или к худшему. Я до сих пор не уверен, почему пара этих программ из предыдущего поста так же съела столько памяти.
Был ли я не прав насчет Java? Своего рода. Это не так плохо, как я думал, но и не на порядок лучше. Его оригинальная философия дизайна была, вероятно, «C ++ без такого количества колючей проволоки», и по этому показателю это определенно успех. Я могу сказать, что это не для меня, но существует как альтернатива тому, что не для меня. И эй, я нашел больше вещей, чтобы обвинить в C ++, что всегда делает меня счастливым.
Мир сильно изменился с момента появления Java. Теперь у нас есть целая конкурирующая экосистема в .NET, появляются JIT, такие как LuaJIT и PyPy, и даже настольное программное обеспечение, написанное на JavaScript и развернутое с собственным небольшим веб-браузером. Java может быть просто ответственна за это широкое признание виртуальных машин и JIT, и я, безусловно, благодарен за это. (Ну … присяжные все еще на Электроне.)
Если вам это показалось интересным, и вы этого раньше не делали, возможно, порадуйте Питона или Руби, чтобы увидеть, откуда я. Оба языка имеют процветающие сообщества, полные очень умных людей, и у обоих очень разные взгляды на то, что значит быть объектно-ориентированным. Python выглядит немного более похожим на Java, хотя под ним скрывается очень гибкая прототипная объектная модель; Ruby использует подход передачи сообщений Smalltalk и добавляет легкую черту Perl. Попробуйте оба, даже; худшее, что может случиться, это то, что вы цените Java еще больше. (Я определенно знаю Python лучше — возможно, когда-нибудь напишу об этом с точки зрения Java или C ++.)
Если у меня есть какие-то советы по поводу расставания с сообществом Java, то это: пожалуйста, вернитесь во времени и вместо этого основывайте Java на Pascal.