Интервью с создателем Java Джеймсом Гослингом
Джеймс Гослинг, более известный как отец языка программирования Java, — специалист в области Computer Science из Канады. Он придумал изначальную архитектуру языка программирования Java, написал для него первый компилятор и виртуальную машину. Наш DevRel, Григорий Петров, взял интервью у Джеймса, и мы приводим полный текст этого интервью в русском переводе. Приятного чтения!
Интервью
Григорий: Мы, как разработчики и консультанты, занимаемся организацией сообществ в России: Python, Ruby, Java, Go. Мы стараемся помогать коллегам тем, что проводим интервью о важнейших вещах в индустрии. Думаю, ваш опыт окажется неоценимым вкладом в это дело и поможет разработчикам стать немного лучше. Давайте начинать!
Некоторые языки, вроде Go, отказываются от классов и наследования, в то время как другие экспериментируют с возможностями вроде traits в Rust. С вашей точки зрения как архитектора языка, какой сейчас наиболее современный, общий и осмысленный способ делать композицию в языках программирования?
Джеймс: Я не думаю, что стоит отказываться от классов. Я обнаружил, что классы довольно хорошо справляются с композицией. У меня даже нет никаких действительно хороших и внятных идей, что тут можно сделать по-другому. Некоторые вещи я сделал бы по-другому, просто потому, что они странные. Например, в С есть макросы, которые скорее проблема, потому что не являются частью языка, а как бы лежат вне его. Ребята из Rust делают большую работу, стараясь вписать макросы в сам язык.
В других языках макросы могут быть вписаны более красиво, вроде всего семейства языков Lisp. Но это потому, что у них есть специальный способ описывать синтаксис, в котором он почти полностью отвязан от семантики. Это неприменимо к большинству остальных языков, в которых синтаксис и семантика очень сильно завязаны друг на друга. Как человек, который в прошлой жизни много писал на Lisp, я очень подсел на технику написания программ на Lisp, манипулирующих другими программами на Lisp. Очень скучаю по этому приёму. Какие-то языки дают сделать похожие вещи альтернативными способами, например в Groovy можно напрямую управлять AST, а в Rust есть что-то вроде синтаксически интегрированных макросов. Но мне всегда казалось, что здесь зарыта интересная тема для исследования: можно ли сделать ещё лучше, продвинуться еще дальше?
Могу ли я получить те же ощущения, что и при разработке на Lisp, используя какие-то вычисления над кодом, чтобы сгенерировать другой код? В Java таким действительно занимаются. Это одна из наиболее популярных возможностей несмотря на то, что она очень низкоуровневая. Людям приходится комбинировать аннотации с генерацией байткода на других языках. Мощная штука. Она используется там, где вы даже бы не подумали — например, в Jackson удалось повысить скорость работы с помощью вычисления сериализатора.
Да, с одной стороны это очень мощная штука, а с другой — ее чертовски сложно использовать. Хорошо, конечно, что это вообще возможно. Но насколько далеко можно зайти? Там есть свои ограничения. Если вы взглянете на что-то вроде Lombok, то эту штуку я считаю... у меня к Lombok появляется сложная смесь любви и ненависти. Он добавляет в Java ряд довольно приятных вещей, но с другой стороны, показывает её слабость. В конце концов, процесс разработки платформы должен был привести к тому, что все эти возможности должны были оказаться встроенными в Java без всякого Lombok. Но Java Community Process, как бы так сказать, перестал быть настолько дружественным к сообществу, каким задумывался, и вот результат. Я уже не в этой теме довольно много лет, но если присмотреться, то повсюду можно найти вещи, которые еще стоит улучшить.
Григорий: Лет пять назад я занимался манипуляциями над байткодом Java. Конечно, с самыми благими целями, для разработки собственных языков, DSL. Оказалось, что это совсем не так просто, как кажется. С помощью Ruby это было бы проще. Evrone специализируется на Ruby, у нас десятки Ruby-разработчиков. И даже с этими крутыми Ruby-разработчиками, требуется много лет, чтобы научиться магии построения DSL.
Джеймс: Да, вычисление фрагментов кода в Java выглядит неудобно, и этому есть причина — Java пытается компилировать всё до машинных кодов. В то время как Ruby по большей части работает на интерпретаторе. Когда ты не пытаешься выжать весь возможный перформанс, всё легко и приятно. Но стоит только попробовать совместить обе стороны одновременно: и получить все мощные возможности языка, и максимальную производительность, всё становится куда сложнее.
Григорий: Недавно мы делали интервью с Юкихиро Мацумото, автором Ruby, и он рассказал об эксперименте с последней большой версией — Ruby 3.0. В этой версии они попытались внести улучшения так, чтобы ничего не сломать — и посмотреть, выйдет ли. Насколько знаю, в Java к вопросам совместимости относятся очень серьёзно. Насколько вообще хороша идея улучшать язык, поддерживая строгую совместимость? Или это какой-то очень узкий подход, который могут позволить себе немногие языки вроде Ruby и Java?
Джеймс: Это практически полностью зависит от размера сообщества разработчиков. Каждое новое изменение, которое что-то ломает, делает больно всему сообществу. Если у тебя не так много программистов, такие изменения не проблема. Кроме того, нужно думать о балансе стоимости и пользы от этих изменений. Меняя что-то, ты добавляешь боли, но одновременно добавляешь и новые возможности. Например, если поменять синтаксис оператора взятия по индексу с квадратных скобок на круглые, ты не получишь никаких осмысленных бонусов, а на разработчиков свалится безумная боль — это очень тупая идея.
Например, в JDK 9 был такое изменение, одно из немногих за всю историю. Если разработчик использует закрытые API, то механизм инкапсуляции ломается. Те, кто ломали границы инкапсуляции и использовали вещи не тем способом, каким они предназначены, получили проблемы в переходе с Java 8 на Java 9. С другой стороны, это изменение позволило аккуратно распиливать платформу на кусочки и делать собственные сборки Java. Например, уменьшить размер Java Runtime Environment.
Другая область, где я испытываю дискомфорт: если в чем-то есть баг и люди подпирают этот баг своими временными решениями и костылями, то попытка исправить баг сломает и все их решения. В процессе разработки Java у нас были моменты, когда мы решили не исправлять баги, а параллельно добавить другой, более правильный способ решения проблемы. Это относится даже к железу — например, проблема с неточными значениями синусов и косинусов, что привело к появлению правильных и неправильных инструкций.
Григорий: Двадцать пят лет назад, когда я начал карьеру разработчика, я писал много кода на С и C++. Я помню кучу мистических проблем с указателями, которые случались раз в месяц. Отладка была очень неприятной. Но сейчас у нас появилось множество инструментов, вроде статической проверки типов. Сейчас разработчики используют IDE вроде NetBeans, IntelliJ IDEA или даже Visual Studio Code. Ты пишешь код, и одновременно с этим инструмент для статической проверки анализирует программу, строит синтаксическое дерево и проверяет всё, что сможет. Ошибки отображаются прямо в редакторе кода. Такие штуки возможны не только в статически-типизированных языках, но и в динамических языках вроде Python, Ruby, TypeScript. Что вы думаете о таких инструментах? Это шаг вперед, позволяющий получать лучшие результаты, или наоборот — индикатор того, что нужно лучше проектировать синтаксис языков?
Джеймс: И то, и другое. Я — фанат языков со статической типизацией, потому что они позволяют нормально работать скаффолдингу для инструментов статической проверки и IDE. Большую часть жизни я был инженером-разработчиком, и за жизнь понял, что самый неприятный способ тратить время — искать странные баги, которые случаются в непонятное время. Хорошо бы, чтобы все эти проблемы исчезали раньше, чем я начну тратить на них время. Поэтому, да, я большой фанат всего, что позволяет IDE снизить вероятность таких багов. Языки с динамической типизацией вроде JavaScript или Python дают куда меньше инструментов для анализа, просто потому, что не всегда точно могут узнать тип, и им приходится догадываться. Языки с сильной типизацией, вроде Java, предоставляют куда более мощный фреймворк для статической проверки. Можно подняться на следующий уровень и вспомнить про автоматическое доказательство теорем. Например, есть пруфер Dafny. Если вы разрабатываете какой-то алгоритм шифрования, то сможете математически проверить его свойства. Для многих разработчиков это слишком, но для каких-то отдельных применений это полезно.
Еще, многое зависит от того, какая у тебя цель. Если ты учишься в университете и пытаешься сдать задания, то тебе достаточно, чтобы программа отработала один раз. Как минимум один раз — потому что тебе нужно продемонстрировать всем, что она действительно работает. Если же ты работаешь в индустрии, а я проработал там большую часть жизни, то однократный запуск — не самая полезная штука. Программа должна работать всегда. Разница между тем, чтобы отработать один раз и работать всегда — огромная. Для однократных применений динамические языки работают довольно хорошо.
Если важно постоянство, то стоит использовать все инструменты для статического анализа, какие найдешь. Но не всегда это нужно. Например, если ты физик и хочешь прямо сейчас получить результат какого-то вычисления, то достаточно сделать его ровно один раз. Всё зависит от контекста. Чем более надежным должен быть софт, тем больше помогают статически типизированные языки.
Григорий: Я никогда сам не программировал роботов, но провел много времени в компаниях, создающих софт для миллионов людей. Можно сравнить то, что было 25 лет назад и сейчас. Сейчас платформы для коллективной разработки, типа GitHub, поддерживаются большими компаниями, и помогают в создании Open Source как индивидуальным разработчикам, так и большим предприятиям. Можно ли считать, что для Open Source сейчас настал золотой век?
Джеймс: Понятия не имею. Вы спрашиваете о будущем, и проблема вопроса «настал ли золотой век» в том, что он неявно подразумевает второй вопрос: «будет ли дальше хуже?». Если сейчас — тот самый век, то будущее у нас не такое уж светлое. Скорее, нам есть к чему еще стремиться. Многое еще стоит улучшить. Сейчас в мире кризис систем безопасности, стало возможным заниматься кибертерроризмом. Когда такое происходит, язык не повернется назвать это золотым веком. Если сообщества найдут способ взаимодействия, который положит конец кибертерроризму — это будет дорогого стоить. Посмотрим. Сейчас отличное время, но может быть лучше.
Григорий: Вы создали Java и JVM (Java Virtual Machine) с JIT (just-in-time compilation). JIT дает отличную производительность, сохраняя синтаксис языка достаточно высокоуровневым. Многие языки последовали по вашим стопам, вроде C# или JavaScript. Скорость JIT-компилированного кода на горячих путях сравнима с С и С++. Но многие другие языки, вроде Python, Ruby, PHP, имеют опциональный JIT, который не так уж и популярен. Многие популярные языки не используют JIT и не получают этого чудесного ускорения. Почему не всем языкам нужен JIT?
Джеймс: Чтобы действительно получить повышенную производительность, очень помогает иметь статически типизированный язык. Для динамических языков, вроде Python, это очень сложно. Обычно люди справляются с этим, добавляя аннотации: так получился TypeScript, который, по сути, все тот же JavaScript, но с аннотациями типов. И это вдвойне забавно, потому что JavaScript — это Java без объявлений типов. По сути, TypeScript — это Java с другим синтаксисом. Но если ты живёшь в мире, в котором нужно быстро склеивать друг с другом готовые скрипты на Python, все эти типы только напрягают. Думать о переменных и их типах — неприятно.
В Python и многих других языках, есть только один числовой тип, и это double. Нет настоящих целых чисел, нет байтов, нет 16-битовых значений, и так далее, всё это добавляет сложность. Но кроме того, оно добавляет и производительность. Если у тебя есть числа с одинарной и двойной точностью — это создает когнитивную нагрузку. Чтобы правильно решать, где что использовать, нужно немного понимать в числовом анализе. А ведь есть достаточно большое количество инженеров, которые об анализе ничего не знают. Они бы предпочли не задумываться о нем лишний раз. Если ты — физик, использующий Python, то почти всегда тебе нужна вся возможная точность, которую может предоставить язык. За исключением случая, когда нужно положить очень большой массив целиком в оперативную память — и вот тут уже разница между типами начинает быть важной. Но если ты живешь в мире, где ничего из этого не происходит, то более простые решения подходят лучше.
Если же нужно об этом беспокоиться... На протяжении жизни я достаточно много изучал числовой анализ и натыкался на ошибки в его применении, чтобы беспокоиться о таких вещах. Все зависит, где ты в этом спектре. Большинству людей, занимающихся скриптами, не важны такие проблемы. У них совсем другие вещи на уме. Им важно ответить на вопрос: достаточно ли их решение быстрое? Производительность в таком случае видится как булевское значение: она или достаточная, или недостаточная. А для каких-то других людей, это больше похоже на тюнинг гоночного автомобиля: если ты добавишь пару километров в час — выиграешь гонку.
Григорий: Несколько месяцев назад Дэвид Хейнемейер Ханссон, создатель Ruby on Rails, рассказывал, что только 15% его облачного бюджета тратится на сам язык. Всё остальное — кэши, очереди сообщений, хранилища, и так далее. Он сказал, что неважно, насколько «медленным» сам по себе является Ruby: даже если Ruby станет в 100 раз быстрее, и 15% превратятся в 1%, это не особо изменит бюджеты. Современные языки «достаточно быстрые».
Джеймс: Зависит от того, что у тебя за программа. Если ты пытаешься решить задачу, в которой доминируют базы данных, сеть и всё такое, и постоянно делаешь RPC, возможно, первый вопрос, который нужно себе задать — так ли тебе нужны эти RPC. Делать микросервисы нормально, но они как минимум в миллион раз медленнее вызова метода. Подумайте, к чему это приводит. Для большинства людей, самый простой способ добиться производительности — сделать чистую масштабируемую архитектуру и масштабироваться. Но есть и люди, которым важны низкоуровневые детали. Если ты занимаешься параллельными вычислениями, возможность запускать тысячи вычислительных процессов одновременно — это важно. Или если ты делаешь что-то вроде своей базы данных, или своего хранилища данных — это очень важно. Всё зависит от задачи.
Григорий: В последнее время множество языков научились работать с корутинами и внедрили подход с async/await для вещей вроде сетевых вызовов. Это есть в Python, в свежей версии Ruby, в JavaScript, во многих других языках. Но async/await, корутины и планировщик — не серебряная пуля. У них есть свои сложности, и иногда они портят производительность. Что вы думаете о хайпе вокруг async/await? Это нормальный способ работать с сетью, или мы используем его не по назначению и стоит посмотреть в сторону Erlang и других подходов?
Джеймс: Это еще один из вопросов, где важен контекст. Корутины сами по себе чудесны, они с нами начиная с 60-х годов. Первым языком с корутинами была Simula 67. Simula была отличным языком. Я очень по ней скучаю. В ней не было тредов, но были корутины — и то, как там работали с корутинами было очень похоже на треды. Но я сам давно не использовал корутины потому, что они не дают преимуществ при использовании нескольких процессоров. Ты не можешь делать настоящий параллелизм.
Но люди работают над этими вопросами в языках, в которых есть настоящий параллелизм, вроде Erlang или Java. Тебе приходится добавлять новый уровень сложности. Обычно, можно бороться с этой сложностью, создав набор хорошо подобранных примитивов. В Java с помощью ConcurrentHashMap можно делать магические вещи. Как только у тебя в руках язык, построенный вокруг корутин, ты пытаешься написать многопроцессорный код, и у тебя не хватает процессоров — ты нагружаешь один процессор. А тебе ведь хочется использовать много процессоров, потому что однопроцессорных систем больше нет, верно? Во всём встроена куча ядер. Если тебе хочется использовать всю мощность своего компьютера, приходится брать все эти вопросы под свой контроль и заниматься настоящей многопоточностью.
А ещё есть проблемы со стилем. Представьте, что вы делаете много await-ов, и по факту они предоставляют прозрачную инверсию контроля, в то время как вызывающий код просто ждёт. Такой синтаксис похож на использование настоящих тредов. Но это же означает появление множества сложных моментов из мира тредов, которые нужно обходить. Если ты говоришь «a = a + 1», ты вероятно знаешь, что в середине этой операции тебя не прервут, и поэтому не нужно делать синхронизацию. Удобно. Но ведь есть другие места, где вместо такой записи нужно переделывать всё на обработку событий, навешивать обработчики завершения. А ведь это основной стиль в JavaScript. Это хорошо работает, но может выглядеть отвратительно.
Когда в начале 70-х я изучал Simula, у неё был хороший, естественный стиль. Ты просто программируешь и думаешь о вычислении как о самозамкнутой сущности. Если сущности пересекаются, для тебя это выглядит прозрачно. Я пришел к выводу, что как концепция этот подход куда лучше, чем программирование вокруг обработки сообщений. Такой подход сложно реализовать, но, когда он реализован, думать в этом формате проще.
Григорий: Simula была первым объектно-ориентированным языком! Тем не менее, в современных языках вроде Ruby, модель многопоточности более сложная: есть процессы, отдельные интерпретаторы в процессах, треды внутри интерпретаторов, и корутины в тредах. Напоминает матрешку. Теперь, нетехнический вопрос, если позволите. Как думаете, какой язык наиболее подходит для обучения новых разработчиков? Возможно, в старшей школе или университете.
Джеймс: Сложно дать беспристрастный ответ. Java успешно используется уже довольно долго. Первый язык программирования, который изучил я сам, был ассемблер PDP-8. Параллельно я смотрел на Fortran. Можно учить людей чему угодно. Какие-то из этих вещей будут восприниматься сложнее, какие-то — проще, но я думаю, что всё сильно зависит от того, по какому карьерному пути собирается идти человек. Если хочется быть разработчиком больших высокопроизводительных систем, вряд ли какие-то языки превзойдут то, что сейчас выполняется на JVM. Не особо важно даже, какой JVM-язык выбрать. В смысле, Scala и Kotlin довольно хороши. Clojure довольно интересная штука, но там нужно по-другому думать. А вот если ты изучаешь физику в университете, то отлично подойдёт Python.
Не думаю, что очень важно, какой язык ты изучишь первым. Многие люди учат что-то одно и потом используют всю жизнь. Но если ты берешь студентов и учишь множеству языков, и переключаешься между ними... Я думаю, каждый университет должен иметь у себя курс, в котором изучается сравнение языков программирования. Например, в семестре у тебя пять задач на пяти разных языках программирования. Это позволяет людям быстро их изучать, они ведь не настолько разные, и позволяет понять, что из них лучше подходит. Я однажды учился на таком курсе и выбрал для каждого задания максимально неправильный язык. Например, делал вычислительные задачи на Cobol, а символьные преобразования — на Fortran. Удивительно, но я всё равно получил максимальный балл.
Григорий: Следующий вопрос про pattern matching. Недавно он появился в Python и Ruby, и множество предложений на эту тему есть в других языках. Мы проверили эту документацию, и теперь не совсем уверены о том, какое место занимает pattern matching в современном высокоуровневом языке. Как вы думаете, как он вписывается в набор инструментов современного разработчика, работающего с Java, Python, Ruby или какого-то другого высокоуровневого языка? Это действительно необходимая штука, или нишевой синтаксис для очень конкретных задач?
Джеймс: Для начала, термин «pattern matching» кажется мне неудачным. Когда я слышу такое сочетание слов, то в голове сразу возникает аналогия с регулярными выражениями, работающими над строками, деревьями, формами деревьев. Возвращаясь к Simula, там был оператор inspect, который очень похож на то, что сейчас называют pattern matching. Конкретней, это оператор выбора, в котором вариантами являются имена типов, например:
Inspect P
When Image do Show;
When Vector do Draw;
Григорий: Последний традиционный вопрос. Российские разработчики гордятся JetBrains и разработанным ими языком Kotlin. Конечно, я не буду сейчас начинать разбор «Java против Kotlin», а спрошу немного другое. Kotlin, как и многие другие языки вроде Clojure и Scala, живут за счет существующей виртуальной машины Java (JVM), и экосистемы библиотек, фреймворков, уже готового кода. Есть ли какие-то проблемы, общие для всех таких языков? Есть ли что-то, что их объединяет, какие-то сложности, связанные с заменой языка Java на какой-то другой?
Джеймс: Зависит от того, что ты пытаешься сделать. Одна из характерных черт JVM — то, насколько глубоко там внедрена идея безопасности и надежности. Например, разработчикам языков приходится иметь дело с целостностью модели памяти. Указатели и всё остальное — что с ними делать? Ты не можешь просто так подделать указатель. Если ты хочешь язык вроде С и не можешь сам создать указатель, ты не можешь реализовать правильный С! На JVM реализовать С сложно — удивительно, но кое-кто это всё равно делает. Впрочем, существуют другие виртуальные машины, с более мягкими гарантиями безопасности, у которых нет жесткой модели выделения памяти. Если хочется сделать интероп между С и Kotlin, придется в какой-то степени пожертвовать безопасностью и надежностью.
Поэтому, всё зависит от того, что ты собираешься делать. Когда я начинал делать Java, одно из моих личных правил состояло в том, что я не хочу отлаживать странные проблемы с испорченной памятью. Я слишком много дней жизни отдал на починку таких багов. Которые зачастую заключаются в том, что кто-то в цикле перезаписал конец массива на один лишний элемент, и ты обнаружишь это много позже, через миллион инструкций. Ненавижу искать такие баги. Всё зависит от того, с чем тебе комфортно жить. Есть люди, которые считают, что тратить на такие штуки время — это очень круто. Кто-то продолжает писать код в vi, который, конечно, был отличным редактором в 70-х, хорошим — в 80-х... а потом прошло пятьдесят лет.
Григорий: Спасибо, Джеймс! Было очень приятно поговорить с вами, и я надеюсь, что в конце этого зомби-апокалипсиса мы сможем наконец-то встретиться вживую на какой-нибудь офлайн-конференции. Спасибо, и приятного дня!
Заключение
Очень приятно, что получилось сделать это интервью с Джеймсом и ближе познакомиться с его взглядом на языки, их возможности и решения, которые мы используем ежедневно.
Также хочется выразить признательность Олегу Чирухину из JetBrains, который помог с подготовкой английской версии и русского перевода этого текста.
Java — неоценимый инструмент, ежедневно помогающий нашим клиентам. Если у вас есть проект в области разработки, с которым вам требуется помощь, или вы хотите больше узнать о наших услугах, оставьте сообщение с помощью формы ниже, и мы скоро с вами свяжемся.