Съдържание на курса
Урок 0 — Какво е Java и какво НЕ е
От Dorian Chávez · основател на Hábil и архитект на интеграции ·
Време: 90 мин
Какво изграждаш: засега нищо. Това е урок за четене: тук се изгражда мисловната карта, с която останалата част от курса става предвидима
Какво научаваш: пътят от изходния код до работещата програма (javac, байткод (bytecode), JVM); какво е JDK и какво беше JRE; какво означава LTS и защо курсът използва Java 25; какво е функция в предварителен преглед (preview); и какво защитава компилаторът на Java и какво оставя да мине (типовете се проверяват при компилация, но генеричните типове се изтриват и null продължава да съществува)
Откъде идваш. Не носиш нищо от този курс: не си инсталирал Java и не си написал нито един ред. Носиш обаче онова, което вече знаеш от друг език —Go, TypeScript, Python или какъвто и да е—: какво е променлива, цикъл, функция, грешка. Точно това курсът приема за известно. Не носиш картата на начина, по който работи платформата на Java, която е различна от платформата на всеки от тези езици и обяснява защо командите, съобщенията за грешки и навиците в Java са такива, каквито са.
Какво не прави този урок. Не инсталира нищо (това е Урок 1) и не преподава синтаксиса на Java (това започва в Урок 2). Съдържа четири малки програми, от четири до осем реда, само за да видиш със собствените си очи онова, което урокът твърди. Не е нужно да ги разбираш ред по ред още сега, макар че всяка е обяснена: важното е какво отпечатват. Изпълняват се след Урок 1, когато вече имаш Java.
За изпълненията. Изходите в този урок са получени с Temurin 25.0.4 (безплатна дистрибуция на Java, която се обяснява по-долу) на macOS, с локал в UTF-8. На твоя Linux Mint излизат същите; единственото, което се променя, и това се казва там, където се случва, са редовете, които споменават операционната система. Програмите са в programas/00-que-es-java/, в хранилището на курса.
След урока ще можеш да
- Описваш със свои думи пътя на един файл
.javaдотам, докато заработи: какво правиjavac, какво съдържа един.classи какво прави JVM с него. - Различаваш JDK, JRE и JVM и казваш кое да инсталираш и защо.
- Обясняваш какво означава, че Java 25 е LTS, защо Java 26 и Java 27 съществуват, но курсът не ги използва, и какво е функция в предварителен преглед.
- Казваш, при една възможна грешка, дали компилаторът на Java я хваща преди изпълнението, или се появява едва когато програмата работи.
- Обясняваш какво е изтриването на генеричните типове и защо
nullпродължава да съществува в език с типове. - Предвиждаш какво отпечатва
"142" + 1в Java и защо това не е събиране.
Защо, преди как
Представи си, че някой ти поиска програмата от курса, revisor: чете списък от услуги, пита всички наведнъж и ти връща отчет. В Go би я написал и би получил изпълним файл, който копираш на сървър и той работи. В Python — скрипт, който работи, докато интерпретаторът е инсталиран. В TypeScript — код, който Node изпълнява, след като компилаторът го преведе на JavaScript. Три езика, три отговора на един и същ въпрос: какво точно се доставя и кой го кара да работи?
Java има свой отговор и това е първото нещо, което трябва да се разбере, защото всичко останало зависи от него. В Java пишеш файлове .java, програма на име javac ги превежда във файлове .class, а друга програма на име java ги изпълнява в рамките на виртуална машина. Две програми, два формата на файл и една виртуална машина: ако не знаеш какво прави всяка от тях, инсталацията от Урок 1 е списък от команди, които се изпълняват на сляпо, а грешките от Урок 2 са съобщения с мистериозен произход.
Освен това има практична причина. В литературата и в ръководствата за Java съжителстват съвети отпреди двадесет години със съвети от днес, и почти никога не казват от коя година са. Който идва без картата, не може да различи „така беше през 2008“ от „така е през 2026“. С картата от този урок можеш.
Урокът има пет части, в този ред:
- Пътят на кода (0.1): от
.javaкъм.classи към JVM. - Частите на платформата (0.2): JDK, JRE и дистрибуциите.
- Версиите (0.3): какво е LTS, какво е предварителен преглед и защо 25.
- Какво защитава компилаторът и какво не (0.4): най-полезната част от урока, с четири програми, които наистина се изпълняват.
- Java в сравнение с онова, което вече познаваш (0.5) и какво предстои в останалата част от курса (0.6).
С това в ума, ето какво ще видиш в този урок, в раздел 0.4.1, когато изпълниш първата програма. Това е съобщение на компилатора, не на изпълнението, и разликата между двете е централната идея:
fig00_01.java:3: error: incompatible types: String cannot be converted to int
int tiempoMs = "142";
^
Понятията
0.1 Пътят на кода: изходен код, байткод и JVM
0.1.1 Трите неща, които съществуват
Когато пишеш програма на Java, има три различни обекта и е добре да им дадем имена още сега:
- Изходният код: текстовият файл, който пишеш, с разширение
.java. Чете го човек. - Байткодът (bytecode): резултатът от компилацията на изходния код, записан във файл с разширение
.class. Това е двоичен, компактен формат, който не е предвиден да го чете човек и не е езикът на никакъв реален процесор. Това е езикът на един въображаем процесор. - JVM (Java Virtual Machine, „виртуална машина на Java“): програмата, която изпълнява байткода. Това е въображаемият процесор, превърнат в реалност, софтуерен компонент, който съществува за всяка операционна система и за всеки тип процесор.
Програмата javac е компилаторът: превръща изходния код в байткод. Програмата java е стартерът: пуска една JVM и ѝ иска да изпълни байткода на един клас. Думата „клас“ ще бъде важна, затова една временна дефиниция: в Java всеки .class съдържа една единица код, наречена клас (по-нататък ще има и други варианти, като record от Урок 3, които също завършват в един .class). Затова се казва „файл на клас“.
Пълната картина на пътя е тази:
Hola.java ──javac──► Hola.class ──java──► [ JVM ] ──► lo que el programa hace
(código fuente) (bytecode) (ejecuta)
Защо две стъпки, а не една? Защото всяка стъпка решава различен проблем и ги решава в различни моменти. Компилаторът работи веднъж, на твоята машина за разработка, и проверява дали написаното има смисъл (например дали не се опитваш да сложиш текст там, където отива число, както ще видиш в 0.4). JVM работи при всяко изпълнение на програмата, на машината, където тя работи, и я кара да функционира с тази операционна система и този процесор. Байткодът е договорът между двете половини: компилаторът обещава да произведе валиден байткод, а JVM обещава да го изпълни.
0.1.2 Един .class отвътре
Всичко това звучи абстрактно, докато не се види. В Урок 1 ще компилираш първата си програма и ще видиш как .class се появява в папката ти. Дотогава погледни какво съдържа един малък. Този фрагмент не е програма от урока: това е минимален клас, Suma, с метод, който събира две цели числа, компилиран с javac Suma.java. После на дизасемблера на JDK, javap, беше поискано да покаже байткода на този метод:
$ javap -c Suma
Compiled from "Suma.java"
class Suma {
Suma();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
static int sumar(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: ireturn
}
Не е нужно да разбираш всеки ред, но обърни внимание на метода sumar, този отдолу. Инструкциите му са iload_0, iload_1, iadd и ireturn: „зареди първия целочислен аргумент“, „зареди втория“, „събери две цели числа“, „върни цяло число“. Това са инструкциите на един прост процесор, от онези, които се програмират със стек: слагат се две числа в стека, събират се, резултатът остава отгоре. Този процесор не съществува в никой реален компютър. Съществува само вътре в JVM. Инструкциите и форматът на файла са определени от публична спецификация, Спецификацията на виртуалната машина на Java, която спазват всички JVM.
Още две данни за файла, измерени с xxd и с javap -v. Първите осем байта на един .class на Java 25 са cafe babe 0000 0045. Първите четири, cafebabe, са магическо число, което идентифицира всеки файл на клас, от всяка версия: спецификацията го определя така. Последните два байта (0045 в шестнадесетичен запис) са основната версия на формата, 69 в десетичен, която съответства на Java 25. Всяка нова версия на Java увеличава това число, а една стара JVM отказва да изпълни .class с по-високо число от това, което разбира. Когато по-нататък видиш съобщение, което говори за „class file version“, това е то.
0.1.3 Какво прави JVM с байткода
JVM не просто „интерпретира“ байткода инструкция по инструкция. Прави три неща и си струва да знаеш кои са, макар да не програмираш нито едно от тях:
- Зарежда и проверява. Чете
.class, проверява дали байткодът е съгласуван (например дали не се опитва да вземе от стека онова, което никога не е сложил) и едва тогава позволява да се изпълнява. Това е мрежа за сигурност срещу повредени или зле направени файлове. - Изпълнява и докато изпълнява, наблюдава. Започва, като интерпретира байткода. Докато програмата работи, открива методите, които се изпълняват много пъти.
- Компилира горещите методи до истински машинен код, в движение, докато програмата продължава да работи. Тази част се нарича JIT компилатор (just in time, „точно навреме“). Това е причината, поради която една програма на Java, след няколко мига загряване, може да работи с бързина, сравнима с тази на програма, компилирана от самото начало.
Освен това JVM управлява паметта: заделя онази, която програмата поиска, и сама я освобождава, когато никой вече не я използва, с част, наречена събирач на боклук (garbage collector). Идеята вече я познаваш, ако идваш от Go, от Python или от TypeScript. Ръководството за JVM на Java 25 и ръководството за събирача на боклук разказват подробностите; за този курс е достатъчно да знаеш, че съществуват и че няма да освобождаваш памет на ръка.
Практическата последица от всичко това е едно известно обещание на Java: „пиши веднъж, изпълнявай навсякъде“. Един и същ .class работи на Linux, на macOS и на Windows, защото всяка система има своя JVM, която знае да го изпълни. Обещанието е истинско, но има граници, и е добре да ги знаеш отсега, за да не му вярваш повече, отколкото трябва. Два измерени примера: името на пътищата, кодировката на текста и краищата на редовете се менят от система на система, а кодът, който ги приема за дадени, се чупи, макар .class да е същият. И текстовият изход зависи от конфигурацията на системата: с минимален локал (LC_ALL=C) една програма, която отпечатва sin conexión (на български: „без връзка“), записа sin conexi?n, защото системата е казала на Java, че изходът ѝ е ASCII; с LC_ALL=C.UTF-8 отпечата правилния текст. От Java 18 текстът на файловете се чете и записва в UTF-8 по подразбиране (JEP 400), но стандартният изход следва конфигурацията на системата. Преносимо не значи идентично.
0.1.4 А java Hola.java, коя от двете стъпки е?
По-нататък ще видиш, че една програма може да се изпълни с една-единствена команда, java Hola.java, без видим javac. Това не противоречи на нищо от казаното. Този режим, наречен стартер на файл с изходен код (source-file launcher) (JEP 330), компилира файла в паметта, без да оставя .class на диска, и го изпълнява веднага. И двете стъпки се случват; просто едната не оставя следа. Удобно е за учене и за програми от един файл; за истински проекти се компилира отделно, и това правиш от Урок 6.
0.2 Частите на платформата: JDK, JRE и дистрибуции
0.2.1 JDK и JRE
Има две съкращения, които ще видиш във всички ръководства и чието значение се промени с годините:
- JRE (Java Runtime Environment) е необходимото, за да се изпълняват програми на Java: JVM и стандартните библиотеки. С JRE можеш да пуснеш
.classили JAR, но не можеш да компилираш. - JDK (Java Development Kit) е JRE плюс инструментите за разработка: компилаторът
javac, дизасемблерътjavap, интерактивният интерпретаторjshell, който ще използваш в Урок 1, пакетиращият инструментjarи други. Папкатаbinна един Temurin 25.0.4 съдържа 30 програми (преброени сls | wc -lи преброени отново една по една в списъка:jar,jarsigner,java,javac,javadoc,javap,jcmd,jconsole,jdb,jdeprscan,jdeps,jfr,jhsdb,jimage,jinfo,jlink,jmap,jmod,jnativescan,jpackage,jps,jrunscript,jshell,jstack,jstat,jstatd,jwebserver,keytool,rmiregistry,serialver).
Този курс изисква компилиране, затова инсталирай JDK. Историческото объркване е, че с години хората инсталираха „Java“ и получаваха JRE (което стигаше, за да се пускат чужди програми, като инсталатор или десктоп приложение), а за да програмират, трябваше да потърсят „JDK“. Днес това разграничение почти няма значение на твоя компютър за разработка, защото инсталираш JDK и толкова; но все още има значение на сървър или в образ на контейнер, където е добре да се инсталира само нужното за изпълнение. Някои дистрибуции все още публикуват пакети само за изпълнение; например страницата за инсталация на Temurin за Linux ги предлага като temurin-<версия>-jre.
И още едно съкращение, което е добре да не се смесва: JVM е само виртуалната машина, компонент, който живее вътре в двете. „Инсталирането на JVM“ не е нещо, което се прави отделно.
0.2.2 Една спецификация, един проект и много дистрибуции
Частта, която най-много обърква онзи, който идва от други езици: няма една-единствена „Java“, която се сваля от едно-единствено място. Има три слоя.
- Спецификацията. Езикът и JVM са определени от публични документи: Спецификацията на езика Java (JLS) и тази на JVM (JVMS), която видя в 0.1. Всеки може да ги реализира.
- OpenJDK. Това е проектът с отворен код, в който се разработва референтната реализация. Всяка нова версия има своя страница в openjdk.org с функциите, които носи, и с графика си. OpenJDK публикува изходен код; не е това, което инсталираш.
- Дистрибуциите. Това са двоичните файлове, които някой изгражда от OpenJDK, тества и публикува със своя политика за актуализации: Eclipse Temurin (на фондацията Eclipse, чрез проекта Adoptium), тези на Oracle, на Azul, на Amazon, тези, които Ubuntu включва в хранилището си, и други. Всички изпълняват един и същ език; различават се по това кой ги изгражда, колко време ги поддържа и при какъв лиценз.
Този курс използва Eclipse Temurin, безплатната дистрибуция на проекта Adoptium на фондацията Eclipse, която няма търговски собственик и публикува ясен график за актуализации: нейната страница за поддръжка казва колко време получава корекции всяка версия и оттам идва таблицата в следващия раздел. Всички изходи на курса са измерени с Temurin 25. Урок 1 я инсталира със SDKMAN, мениджър на версии, който работи в личната ти папка, без sudo, и който в Урок 6 инсталира и Maven. И обяснява между другото защо не се използва default-jdk, пакетът, който Linux Mint ти предлага. Предварително обяснение: в Mint 22 този пакет инсталира Java 21, а не Java 25.
0.3 Версиите: ритъм, LTS и предварителен преглед
0.3.1 Нова версия на всеки шест месеца
Java престана да пуска голяма версия на няколко години и възприе постоянен ритъм: нова версия на всеки шест месеца, през март и през септември. Така казва страницата за поддръжка на Temurin: „OpenJDK provide a new feature release every six months“. Всяка носи малко функции, замислени така, че промяната да е малка.
Последицата е, че версиите са много, а повечето спират да получават актуализации след шест месеца. Това би било неуправляемо за онзи, който поддържа програма в продукция. Затова съществува понятието LTS.
0.3.2 LTS: версията, която се поддържа с години
LTS е съкращение от Long-Term Support, „дългосрочна поддръжка“. LTS версия е тази, която производителите се ангажират да поддържат, с корекции за сигурност, в продължение на години. Страницата за поддръжка на Temurin го описва така: на всеки две години, от 2021 г., една версия се обявява за LTS, а Temurin се ангажира да я поддържа най-малко четири години. Сметките, които са публикувани там (проверени на 7 октомври 2026 г.):
| Версия | Излиза | Вид | Temurin я поддържа |
|---|---|---|---|
| Java 17 | септември 2021 г. | LTS | най-малко до октомври 2027 г. |
| Java 21 | септември 2023 г. | LTS | най-малко до декември 2029 г. |
| Java 25 | септември 2025 г. | LTS | най-малко до септември 2031 г. |
| Java 26 | март 2026 г. | не е LTS | приключи през септември 2026 г. |
| Java 27 | 15 септември 2026 г. | не е LTS | до март 2027 г. |
Таблицата отговаря на въпроса „защо Java 25, след като вече има 27?“. Защото 27 не е LTS: след шест месеца ще спре да получава корекции и ще трябва да се прескочи към друга. Курс, който се публикува днес и се чете с години, не бива да учи върху основа, която изтича за половин година. 25 е действащата LTS и тя е онази, която всеки сериозен екип ще има в продукция през следващите години. Ако двугодишният ритъм се запази, следващата LTS ще бъде 29, през септември 2027 г.; това е екстраполация на този ритъм, а не обещание на някого.
Две уточнения, които предотвратяват объркване. Първо, „LTS“ не значи „без промени“: 25 получава корекции за сигурност и за грешки, но не и нови функции. Второ, номерата на кръпките —онова, което идва след 25. в това, което отпечатва java --version, което започва с 25— се менят на всеки три месеца; затова в курса се казва „Java 25“ и никога номер на кръпка.
0.3.3 Предварителният преглед: онова, което съществува, но още не е окончателно
Java въвежда новите функции през един междинен етап: предварителен преглед (preview), описан в JEP 12. Една функция в предварителен преглед вече е специфицирана и програмирана, но екипът на Java си запазва правото да я промени или премахне в следващата версия, според онова, което научи от тези, които я изпробват. За да не позволи на някого да я използва случайно, компилаторът я отхвърля, освен ако изрично не му се поиска с флага --enable-preview.
Предварителният преглед има конкретна последица, която си струва да се види с реален случай, този на структурираната конкурентност (structured concurrency), която в Java 25 все още е в предварителен преглед (JEP 505). Тази програма я използва:
import java.util.concurrent.StructuredTaskScope;
void main() throws Exception {
try (var alcance = StructuredTaskScope.open()) {
var tarea = alcance.fork(() -> "hola");
alcance.join();
IO.println(tarea.get());
}
}
Без флага компилаторът отказва. Ето какво отпечатва, точно така (това е фрагмент, а не фигура от курса, защото умишлено не се компилира):
$ java Preview.java
Preview.java:1: error: StructuredTaskScope is a preview API and is disabled by default.
import java.util.concurrent.StructuredTaskScope;
^
(use --enable-preview to enable preview APIs)
Preview.java:4: error: StructuredTaskScope is a preview API and is disabled by default.
try (var alcance = StructuredTaskScope.open()) {
^
(use --enable-preview to enable preview APIs)
2 errors
error: compilation failed
Съобщението е ясно: „е API в предварителен преглед и е изключено по подразбиране“. С java --enable-preview --source 25 Preview.java същата програма отпечатва hola („здравей“). Но има уловка, която не се вижда в съобщението: един .class, компилиран с функции в предварителен преглед, носи в заглавката си отметката „предварителен преглед“, измерена с javap -v: minor version: 65535, вместо обичайната 0. Тази отметка прави така, че само точната версия на Java, с която е компилиран, може да го изпълни. Програма, изградена върху функция в предварителен преглед на 25, може да спре да работи при преминаване към 26. Затова курсът определя правило, което не се обсъжда: нищо в предварителен преглед не се използва в кода на курса. Структурираната конкурентност се появява в Урок 9 като каре „какво предстои“, с флага си и с предупреждението да не се използва в продукция; останалото е само онова, което вече е окончателно в 25.
0.4 Какво защитава компилаторът и какво не
Това е най-важният раздел на урока и този, който най-много прилича на минно поле. Java се хвали, че е език със статични типове, тоест че компилаторът проверява типовете преди програмата да работи. Вярно е и е огромно предимство. Но „статични типове“ не значи „без грешки при изпълнение“ и има четири дупки, които е добре да се познават от първия ден. Всяка идва с програма, която да изпълниш след Урок 1.
0.4.1 Което компилаторът хваща: типовете
Да започнем с онова, което защитава. Програмата fig00_01.java декларира променлива, която трябва да съдържа цяло число, и ѝ присвоява текст:
// fig00_01.java
void main() {
int tiempoMs = "142";
IO.println(tiempoMs);
}
Тази програма не работи: компилаторът я отхвърля преди това. Изпълни я с java fig00_01.java и получаваш:
$ java fig00_01.java
fig00_01.java:3: error: incompatible types: String cannot be converted to int
int tiempoMs = "142";
^
1 error
error: compilation failed
Чети го отгоре надолу, както ще го четеш стотици пъти. Първият ред носи три данни: файлът (fig00_01.java), номерът на реда (3) и диагнозата (incompatible types: String cannot be converted to int, „несъвместими типове: String не може да се преобразува в int“). Отдолу компилаторът копира проблемния ред и поставя стрелка ^ в точката, където е забелязал проблема. В края 1 error брои проблемите, а error: compilation failed е стартерът, който казва, че не е стигнал да изпълни нищо. Обърни внимание и на онова, което не се случи: програмата не отпечата нищо, дори частичен резултат. Грешката при компилация спира всичко преди първата инструкция.
Редовете на програмата, за да не останат загадка: void main() е начинът да започне една програма (Урок 1 го обяснява подробно), int tiempoMs декларира променлива от целочислен тип, а IO.println отпечатва един ред. Важното е сравнението с другите ти езици. В Python еквивалентът (tiempo_ms: int = "142") работи, без да се оплаче, защото анотациите не се проверяват при изпълнение. В TypeScript компилаторът го хваща точно както Java. В Go — също. Java е в групата, която го хваща преди изпълнението.
0.4.2 Първата дупка: генеричните типове се изтриват
Генеричните типове (generics) са синтаксисът на Java, за да се каже „списък от текстове“, „списък от числа“: ArrayList<String>, ArrayList<Integer>. (Темата е на Урок 4; тук интересува само една последица.) Компилаторът използва тази информация, за да провери кода ти. Но след компилацията информацията изчезва: компилаторът я изтрива и оставя байткода без нея. Това се нарича изтриване на типовете (type erasure).
Показва го fig00_02.java, която създава списък от текстове и списък от числа и пита дали са от един и същ клас:
// fig00_02.java
import java.util.ArrayList;
void main() {
var palabras = new ArrayList<String>();
var numeros = new ArrayList<Integer>();
IO.println(palabras.getClass() == numeros.getClass());
IO.println(palabras.getClass().getName());
}
$ java fig00_02.java
true
java.util.ArrayList
Програмата, ред по ред. Първият ред, import, казва на компилатора къде да намери ArrayList: той е в пакета java.util на стандартните библиотеки. var иска от компилатора да изведе типа на променливата от стойността, която ѝ се присвоява (Урок 2 го обяснява). Двете new ArrayList<...>() създават празен списък, единият обявен като от текстове, а другият като от числа. getClass() връща класа на един обект по време на изпълнение, а == сравнява дали са един и същ. Отговорът е true: за JVM двата списъка са просто един java.util.ArrayList. <String> и <Integer> вече не съществуват, когато програмата работи.
Защо е проектирано така? Заради съвместимостта. Генеричните типове идват в Java във версия 5, а дотогава съществуват милиони редове код и библиотеки без тях. Изтриването на информацията позволи старият и новият код да съжителстват. Самата спецификация признава това наследство, когато говори за „суровите типове“ (raw types), които се допускат само „като отстъпка в името на съвместимостта със стария код“ (JLS, раздел 4.8). Едно странично предимство, което отбелязва официалното ръководство, е, че генеричните типове не струват нищо при изпълнение: не се създават нови класове за всяка комбинация.
Последицата за теб е, че някои неща, които би очаквал от език с генерични типове, не могат да се правят в Java: да попиташ при изпълнение „това списък от текстове ли е?“ няма отговор, защото тази информация не съществува. Когато стигнеш до Урок 4 и срещнеш странни ограничения с генеричните типове, причината почти винаги е това изтриване. Сравни го с TypeScript, където всички типове се изтриват при генерирането на JavaScript. В Java се изтриват само аргументите на генеричните типове; класът на обекта оцелява, и затова програмата можа да отпечата java.util.ArrayList.
0.4.3 Втората дупка: null продължава да съществува
Java има типове, но не пречи една променлива от обектен тип, като String, да е „нищо“. Това „нищо“ се пише null. Компилаторът приема да го присвоиш и не те предупреждава, когато го използваш зле. fig00_03.java декларира детайл на услуга, оставя го null и иска дължината му:
// fig00_03.java
void main() {
String detalle = null;
IO.println("antes de usar el detalle");
IO.println(detalle.length());
IO.println("esto no se imprime");
}
$ java fig00_03.java
antes de usar el detalle
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local1>" is null
at fig00_03.main(fig00_03.java:5)
Тази програма се компилира без проблеми. Грешката се появява едва когато се изпълни петият ред, и затова първият IO.println успя да отпечата. Това е разликата между грешка при компилация (0.4.1: не се изпълнява нищо) и грешка при изпълнение: програмата работи, докато се натъкне на проблема, и спира там. Името на грешката, NullPointerException, значи „изключение за нулев указател“; изключението е начинът на Java да съобщи, че нещо е тръгнало на зле по време на изпълнението (Урок 5 го разглежда изцяло). Съобщението ѝ казва какво се опита да направиш (Cannot invoke "String.length()", „не може да се извика String.length()“) и защо се провали (because "<local1>" is null, „защото <local1> е нулев“). От Java 15 JVM дава подробни съобщения като това по подразбиране, благодарение на JEP 358.
Може да те изненада, че променливата се казва <local1>, а не detalle. Това не е грешка на урока: когато изпълняваш файл директно с java Archivo.java, вътрешният компилатор не запазва имената на локалните променливи. Урок 2 показва как да ги възстановиш с друг начин на компилиране. Важното днес е идеята: компилаторът на Java не те защитава от null.
Самият автор на null, Тони Хоар, съжалява, че го е измислил: през 2009 г. го нарече „моята грешка за милиард долара“ (лекция), заради повредите, които е причинявал с десетилетия. Java не го премахна, но с годините предлага инструменти, за да не зависиш от него: в Урок 3 запечатаните типове заместват null в revisor с тип, който изрично казва „имаше отговор“ или „нямаше“, а в Урок 4 Optional покрива случаите „понякога има данни“.
0.4.4 Третата дупка: + не винаги събира
Третата е капан за онзи, който идва от друг език. fig00_04.java използва оператора + по три начина:
// fig00_04.java
void main() {
String tiempo = "142";
IO.println(tiempo + 1);
IO.println(1 + 42 + "ms");
IO.println("ms" + 1 + 42);
}
$ java fig00_04.java
1421
43ms
ms142
И трите резултата идват от едно-единствено правило: ако някоя от двете страни на + е текст, + слепва; ако и двете са числа, събира; и се оценява отляво надясно. Първият ред слепва текста "142" с числото 1 и дава "1421", текст от четири знака, а не числото 143. Вторият първо събира 1 + 42 (и двете са числа) и дава 43, и едва тогава го слепва с "ms". Третият започва с текст, така че "ms" + 1 вече е слепване и дава "ms1", а после "ms1" + 42 дава "ms142". Правилото е в спецификацията на езика.
Онова, което прави този случай опасен, не е, че е незаконен, а че се компилира без нито едно предупреждение и дава правдоподобен резултат. Ако в revisor някое време пристигне като текст, "142" + 1 няма да те предупреди за нищо: ще произведе "1421" и тази невярна стойност ще пропътува до отчета. В Python "142" + 1 е TypeError веднага; в TypeScript дава "1421" точно както в Java. Java е на страната на TypeScript в това отношение и затова Урок 2 настоява всяка стойност на revisor да има правилния тип от момента, в който влиза.
0.4.5 Четвъртата дупка: онова, което компилаторът никога няма да види
Предишните три могат да се предизвикат с програми от няколко реда. Има и четвърта, по-широка група: стойностите, които са известни само при изпълнение. Компилаторът проверява типове; не проверява стойности. Тези фрагменти бяха изпълнени и ето какво отпечатват:
void main() {
int maximo = Integer.MAX_VALUE;
IO.println(maximo + 1);
}
$ java Desborde.java
-2147483648
Едно цяло число в Java има фиксиран размер (четири байта) и ако едно събиране надхвърли максимума, резултатът се завърта до минимума, без грешка. Спецификацията го казва направо: „целочислените оператори не указват препълване по никакъв начин“ (JLS, раздел 4.2.2). Още два случая, измерени по същия начин, всеки в свой файл с void main(), който съдържа само посочения ред:
IO.println(1 / 0);
$ java Division.java
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Division.main(Division.java:2)
IO.println("hola".charAt(10));
$ java Caracter.java
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: Index 10 out of bounds for length 4
at java.base/jdk.internal.util.Preconditions$1.apply(Preconditions.java:55)
...
at Caracter.main(Caracter.java:2)
Многоточията заместват девет вътрешни реда на JDK, чиито номера се менят с всяка актуализация. Делението на цяло число на нула е изключение при изпълнение; искането на знак номер 10 от текст с четири знака — също. И в двата случая компилаторът не можеше да го знае, без да изпълни програмата. Обобщението на раздела се събира в една таблица:
| Какво | Кога се открива? | Пример от този урок |
|---|---|---|
| Текст там, където отива число | При компилация | int tiempoMs = "142"; (fig00_01) |
Един null, използван като обект |
При изпълнение | detalle.length() (fig00_03) |
Един +, който слепва вместо да събира |
Никога: резултатът е валиден | "142" + 1 (fig00_04) |
| Типът на елементите на един списък | При компилация, а после се изтрива | ArrayList<String> (fig00_02) |
| Цяло число, което надхвърля максимума си | Никога: завърта се | Integer.MAX_VALUE + 1 |
| Целочислено деление на нула | При изпълнение | 1 / 0 |
Останалата част от курса се опитва да направи тази средна колона възможно най-ранна: да премести грешките от „никога“ или „при изпълнение“ към „при компилация“. Запечатаните типове от Урок 3 правят така, че забравянето на един случай да е грешка на компилатора. Неизменяемите колекции от Урок 4 пречат на случайни промени. Тестовете от Урок 7 хващат онова, което компилаторът не може. Не е случайност: това е методът.
0.5 Java в сравнение с онова, което вече познаваш
С предишната карта сравнението с другите езици на курса престава да е смътно. Ето какво се променя и какво не:
| Go | TypeScript | Python | Java | |
|---|---|---|---|---|
| Какво произвежда компилаторът? | изпълним файл за една система и един процесор | JavaScript (типовете изчезват) | байткод за интерпретатора, автоматично | байткод за JVM, в изрична стъпка |
| Кога се проверяват типовете? | при компилация | при компилация (с tsc) |
не се проверяват при изпълнение | при компилация |
| Какво изпълнява програмата? | процесорът директно | Node или браузър | интерпретаторът на Python | JVM |
| Има ли събирач на боклук? | да | да (двигателят на JavaScript) | да | да |
| Как се доставя? | копира се изпълнимият файл | копират се JavaScript и Node | копират се кодът и интерпретаторът | копира се един JAR (файл с .class вътре) и трябва да има JVM |
Две неща правят впечатление. Първото: ако идваш от Python, моделът „изходен код, байткод, виртуална машина“ вече го познаваш; и Python компилира до байткод и го изпълнява във виртуална машина. Онова, което се променя в Java, е, че стъпката на компилиране е изрична и видима и че по средата проверява типовете. Второто: ако идваш от Go, разликата е JVM като посредник. Изпълнимият файл на Go носи всичко в себе си; програма на Java има нужда от инсталирана JVM там, където работи, в замяна на преносимостта между системи със същия файл.
Съществува и път за получаване на нативен изпълним файл от програма на Java (предварителна компилация с GraalVM). Не се използва в този курс. Блогът на Hábil обяснява кога е удачно и кога не да се компилира Java до нативен код; прочети го, когато завършиш Урок 10, не преди това.
0.6 Какво предстои в останалата част от курса
Всичко казано дотук се прилага на практика в следващите уроци. Курсът изгражда една-единствена програма, revisor, и всеки урок добавя по една част:
| Урок | Какво се добавя | Коя дупка от този урок помага да се затвори |
|---|---|---|
| 1 | Инсталираш Java 25 и пишеш и компилираш първата си програма | пътят от 0.1, в твоя терминал |
| 2 | Типове, низове и управление на потока | null, == срещу equals, +, което слепва |
| 3 | record, enum и запечатани типове |
null, заменен с типове, които назовават всичките си случаи |
| 4 | Колекции, генерични типове и стриймове (streams) | изтриването на типовете, списъци, които не се променят |
| 5 | Грешки и изключения | грешките при изпълнение, обработени вместо търпени |
| 6 | Проект с Maven (инструментът, който изгражда проекти на Java) | компилираното като продукт: пакети, един pom.xml и един .jar |
| 7 | Тестове с JUnit | онова, което компилаторът не вижда, уловено от тестове |
| 8 | HTTP и JSON | данни, които идват отвън и трябва да се валидират |
| 9 | Конкурентност (concurrency) | проверка на всички услуги наведнъж |
| 10 | Завършената програма | команден ред, сървър, качество и един JAR |
Има още една практическа уговорка. Програмите от Уроци 1 до 5 се изпълняват с java Archivo.java, без допълнителни инструменти. От Урок 6 проектът живее в стандартна структура, с Maven. Курсът използва отстъп от два интервала в целия код, този на ръководството за стил на Google за Java, защото в Урок 10 един инструмент проверява този стил и би било абсурдно тогава да признаем, че всичко предишно е било написано по друг начин. Много код в интернет използва четири интервала; работи същото.
Грешката, която ще видиш
Този урок има една основна грешка, тази на компилатора, който отказва, и една при изпълнение, която е добре да се разпознава от пръв поглед. И двете са от предишните раздели.
Основната грешка: incompatible types. Тя е от fig00_01.java:
fig00_01.java:3: error: incompatible types: String cannot be converted to int
int tiempoMs = "142";
^
Чете се така: файлът и редът (fig00_01.java:3), какво се случи (String не може да се преобразува в int), проблемният ред и стрелката в точната точка. Поправя се по два начина, според онова, което си искал: ако стойността е била текст, променливата е трябвало да е от тип String; ако си искал числото, текстът трябва да се преобразува изрично (с това се занимава Урок 2). Онова, което не се прави, е да се пренебрегне: грешка при компилация никога не се „подминава“, защото програмата просто не работи.
Грешка при изпълнение, която вече можеш да разпознаеш: NullPointerException.
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local1>" is null
Чете се отляво надясно: в главната нишка (main) възникна NullPointerException; опита се да извикаш String.length() и не стана, защото в променливата имаше null. Отдолу редът, който започва с at, казва къде: fig00_03.main(fig00_03.java:5), тоест методът main на файла, ред 5. Прегледай този ред и се запитай откъде е могъл да дойде един null.
Какво се прави погрешно
Да инсталираш „Java“, без да знаеш какво се инсталира. Който напише sudo apt install default-jre, получава само частта за изпълнение, и в Mint 22 от версия, по-стара от тази, която използва курсът. За програмиране — JDK; за курса — този на Java 25.
Да използваш най-новата версия само защото е най-новата. Java 27 излезе на 15 септември 2026 г. и не е LTS. За учене, за сървър или за програма, която ще живее години, действащата LTS. Ако вече имаш инсталирана 27, нищо сериозно няма да стане с този курс, но Урок 1 ти казва как да се увериш коя версия използваш.
Да вярваш, че „има типове“ значи „не се проваля при изпълнение“. Типовете хващат едно семейство грешки, не всички. Тези от 0.4 съществуват във всяка програма на Java, направена от когото и да е.
Да копираш ръководство, без да погледнеш датата му. В интернет циркулират версии на „Hola mundo“ отпреди двадесет години, които налагат да се напише клас и метод public static void main(String[] args). Работят, но за файл с една-единствена програма вече не е нужно нищо от това, както ще видиш в Урок 1. Същото важи за библиотеките: почти всичко публикувано за JSON или тестове е писано за версии, по-стари от тези на курса. Когато една рецепта не се компилира, първото, което се проверява, е версията.
Да използваш функция в предварителен преглед „защото работи“. Работи днес, с флага, в точно тази версия. Според JEP 12 може да се промени или да изчезне в следващата. Не в код, който някой друг ще поддържа.
Да бъркаш Java с JavaScript. Споделят букви от името и половината фигурни скоби, и нищо повече. Те са различни езици, с различни модели на изпълнение и с историческа връзка, която никому не е нужно да знае, за да използва единия от двата.
Да очакваш „пиши веднъж, изпълнявай навсякъде“ да включва средата. .class е преносим; кодировката на изхода, пътищата и краищата на редовете не са сами по себе си. Измерва се, не се предполага.
Упражнения
Упражненията в този урок не изискват Java: първото е за четене, второто се отговаря преди да пуснеш каквото и да е и се проверява след Урок 1, а третото и четвъртото се отговарят писмено, със свои думи. Най-добрият начин да се възползваш от второто е да запишеш предположението си в дневника и да го сравниш после.
Упражнение 1 — Данните на revisor (проект)
Това е таблицата, която revisor ще произвежда в края на курса. За всяка колона посочи дали стойността е число, текст, или може да е нищо. Обърни внимание най-вече на времето на reportes.
SERVICIO ESTADO TIEMPO DETALLE
catalogo ARRIBA 142ms 200
inventario LENTO 2.3s 200
pagos ARRIBA 87ms 200
reportes CAIDO — sin conexión
След това отговори: какво би направила Java с "142" + 1, ако някое време пристигне като текст? И какъв проблем би имало, ако една програма запази времето на reportes като null и после се опита да го форматира?
Упражнение 2 — Кога програмата разбира?
За всеки от тези шест фрагмента предвиди дали (а) компилаторът го отхвърля, (б) програмата се компилира и пада при изпълнение, или (в) се компилира, работи и дава резултат без предупреждение. Ако е (б), кажи какво изключение очакваш.
int tiempoMs = "142";String detalle = null;, следвано отdetalle.length().Integer.MAX_VALUE + 1var palabras = new java.util.ArrayList<String>();, следвано отpalabras.add(5);1 / 0(между цели числа)."hola".charAt(10)
Упражнение 3 — Избери версията
Реши коя версия на Java би използвал във всеки случай и защо: (а) нова програма, която екипът ти ще поддържа няколко години; (б) имаш инсталирана Java 27 и искаш да следваш този курс; (в) една статия използва StructuredTaskScope и искаш да я изпробваш на машината си.
Упражнение 4 — Обясни го с пет изречения
Колега, който програмира на Go, те пита какво се случва между „написах един .java“ и „програмата работи“. Отговори му с най-много пет изречения, като използваш думите изходен код, javac, байткод, JVM и JIT компилатор. Без да гледаш урока.
Решения
Решение 1
Колоните, по ред:
- SERVICIO е текст (
catalogo,pagos...). ESTADO е текст, който може да има само една от три стойности (ARRIBA,LENTO,CAIDO); в Урок 3 става собствен тип, който не допуска други. - TIEMPO е число (142, 2300, 87), което се показва с единица (
142ms,2.3s) и може да е нищо:reportesне отговори и няма време. Тази черта—представя стойност, която не съществува. - DETALLE е текст, макар да изглежда като число:
200е кодът на състоянието, но в таблицата се показва като текст, също катоsin conexión(„без връзка“).
С "142" + 1 Java не събира: слепва и дава текста "1421" (fig00_04). Тъй като се компилира без предупреждение, една невярна стойност би влязла в отчета, без никой да разбере. А с времето на reportes, запазено като null, форматирането му с нещо като tiempo.length() хвърля NullPointerException при изпълнение (fig00_03), защото компилаторът не защитава от това. Двете ситуации са причината Уроци 2 и 3 да съществуват: във 2 се учи да се пише кодът внимателно спрямо null, а в 3 дизайнът се променя така, че „нямаше отговор“ да е изричен случай на типа, а не скрит null.
Решение 2
След Урок 1 можеш да ги провериш с jshell или с файл на програма. Отговорите, измерени с Temurin 25.0.4:
- (а) Компилаторът го отхвърля:
incompatible types: String cannot be converted to int(fig00_01). - (б) Компилира се и пада при изпълнение с
java.lang.NullPointerException(fig00_03). - (в) Компилира се, работи и дава
-2147483648, без предупреждение: цялото число се завърта. - (а) Компилаторът го отхвърля. Това е стойността на генеричния тип: списъкът е от текстове, а
5не е. Съобщението започва сerror: no suitable method found for add(int)и продължава със списък на вариантите наadd, които е опитал, всеки със своята причина (тази, която има значение, гласиargument mismatch; int cannot be converted to String). Това е пример, че компилаторът използва генеричните типове, макар после да ги изтрива. - (б) Компилира се и пада при изпълнение с
java.lang.ArithmeticException: / by zero. - (б) Компилира се и пада при изпълнение с
java.lang.StringIndexOutOfBoundsException: Index 10 out of bounds for length 4.
Ако си сгрешил в 3, нормално е: това е единственото, при което програмата „работи“ и дава грешно число, и затова е най-опасното от шестте.
Решение 3
(а) Java 25, действащата LTS: ще има корекции за сигурност най-малко до септември 2031 г. Една не-LTS версия би спряла да се поддържа след шест месеца.
(б) Не е нужно да деинсталираш нищо, но не е добре и да я използваш за курса. Java 27 не е LTS, а курсът проверява програмите си с 25; сигурното е да инсталираш Temurin 25 (със SDKMAN, Урок 1, двете съжителстват и избираш която да използваш) и да провериш с java --version коя е активна. Онова, което не бива да се прави, е да се предполага, че „най-новата върши работа за всичко“.
(в) Във версия 25, с --enable-preview, като знаеш, че е функция в предварителен преглед: API може да се промени в следващата версия (всъщност се промени между 25 и 26, според JEP 525), получилият се .class работи само в точно тази версия и не бива да се използва в програма, която друг ще поддържа.
Решение 4
Един възможен отговор: „Пишеш изходния код в един .java. Компилаторът javac го проверява (най-вече типовете) и го превежда до байткод, който отива в един .class. Този байткод не е код на реален процесор: изпълнява го JVM, която съществува за всяка операционна система. Докато програмата работи, JVM открива методите, които се използват много, и ги превръща в машинен код с JIT компилатора. Затова един и същ .class работи във всяка система, която има JVM, за разлика от изпълним файл на Go, който вече носи кода на своя процесор“.
Как разбирам, че съм успял
- Рисуваш по памет пътя
.java→javac→.class→java→ JVM и казваш какво прави всяка стрелка. - Казваш разликата между JDK, JRE и JVM и кое се инсталира за този курс.
- Обясняваш защо има Java 27, а курсът използва 25, с думата LTS.
- Знаеш какво е функция в предварителен преглед и какъв флаг изисква.
- За всеки ред от таблицата в 0.4.5 казваш кога се открива проблемът.
- След Урок 1 пускаш
java fig00_01.java,fig00_02.java,fig00_03.javaиfig00_04.javaи изходите ти съвпадат с тези от този урок, освен пътя. - Предвиждаш в дневника какво отпечатва
"ms" + 1 + 42, преди да го изпълниш, и проверяваш. - Записал си в дневника си едно нещо, което си вярвал за JVM и се е оказало различно.
Обобщение
Отговори, без да гледаш урока:
- Какво съдържа един
.classи кой го изпълнява? - Какво добавя JDK към JRE? Защо в този курс се инсталира JDK?
- Какво е OpenJDK и какво е дистрибуция като Temurin?
- Какво значи LTS? Защо Java 27 не е версията на курса?
- Какво се случва с един
.class, компилиран с функции в предварителен преглед? - С какво се различават грешка при компилация и грешка при изпълнение? Дай по един пример.
- Какво е изтриването на генеричните типове и какво доказва
fig00_02.java? - Защо
"142" + 1е"1421", а не143? - Защо компилаторът на Java не те предупреждава за зле използван
null?
За допълнително четене
- JEP 512, „Compact Source Files and Instance Main Methods“: предложението, което прави така, че в Java 25 една програма да може да се напише без клас и без
public static, което е стилът на този курс. Чете се по-добре след Урок 1. Посетено на 7 октомври 2026 г. - JEP 12, „Preview Features“: какво е предварителният преглед, защо съществува и какви гаранции не дава. Посетено на 7 октомври 2026 г.
- Спецификация на виртуалната машина на Java, SE 25, глава 4: точният формат на файла
.class, включително магическото число и версията. Посетено на 7 октомври 2026 г. - Temurin Support, Adoptium: графикът на версиите и краят на поддръжката на всяка от тях, откъдето идва таблицата от 0.3. Посетено на 7 октомври 2026 г.
Предпочитате имейл? Пишете ни на hola@habil.mx