1999 год. Алматы. Типичное рабочее подключение к интернету - dial-up через модем на 33 кбит/с. Веб-страница с тремя-четырьмя картинками грузится 15-20 секунд. Скачать 200-килобайтный JAR-файл с апплетом - ещё минута сверху, плюс время на инициализацию JVM в браузере.
Мы это делали. Потому что Java-апплеты предлагали то, чего нельзя было добиться ни HTML, ни JavaScript: настоящую графику в реальном времени, анимацию, реакцию на ввод пользователя без перезагрузки страницы. Для корпоративных сайтов, которым нужна была интерактивная карта офиса или живой график курсов валют - это было единственным решением.
JDK 1.1 и AWT: основа апплетов 1999 года
// CurrencyTickerApplet.java - бегущая строка с курсами валют
// JDK 1.1, AWT, 1999 год
// Актуальное применение для казахстанских финансовых сайтов
import java.applet.Applet;
import java.awt.*;
import java.awt.event.*;
public class CurrencyTickerApplet extends Applet implements Runnable {
// Данные из <param> тегов в HTML - курсы передавались вручную
private String tickerText = "USD/KZT 130.00 EUR/KZT 138.50 RUB/KZT 5.80";
private int scrollSpeed = 2;
private Color bgColor = Color.black;
private Color textColor = new Color(255, 215, 0); // золотой - стиль финансового сайта
private Thread animThread;
private int xPosition;
private Font tickerFont;
private int textWidth;
@Override
public void init() {
String p = getParameter("rates");
if (p != null) tickerText = p;
String s = getParameter("speed");
if (s != null) scrollSpeed = Integer.parseInt(s);
tickerFont = new Font("Monospaced", Font.BOLD, 13);
setBackground(bgColor);
FontMetrics fm = getFontMetrics(tickerFont);
textWidth = fm.stringWidth(tickerText);
xPosition = getSize().width;
}
@Override
public void start() {
if (animThread == null || !animThread.isAlive()) {
animThread = new Thread(this);
animThread.start();
}
}
@Override
public void stop() {
animThread = null;
}
@Override
public void run() {
while (Thread.currentThread() == animThread) {
xPosition -= scrollSpeed;
if (xPosition < -textWidth) xPosition = getSize().width;
repaint();
try { Thread.sleep(40); } catch (InterruptedException e) { break; }
}
}
@Override
public void paint(Graphics g) {
g.setFont(tickerFont);
g.setColor(textColor);
g.drawString(tickerText, xPosition, getSize().height / 2 + 5);
}
// Двойная буферизация - без этого в AWT 1999 мерцало нещадно
private Image buf;
private Graphics bufG;
@Override
public void update(Graphics g) {
Dimension d = getSize();
if (buf == null) {
buf = createImage(d.width, d.height);
bufG = buf.getGraphics();
}
bufG.setColor(bgColor);
bufG.fillRect(0, 0, d.width, d.height);
paint(bufG);
g.drawImage(buf, 0, 0, this);
}
}
Компиляция и упаковка:
# JDK 1.1 на Windows 98 (стандартная рабочая машина разработчика в Алматы 1999)
# JDK скачивали с java.sun.com, размер ~10 МБ - несколько часов по dial-up
javac CurrencyTickerApplet.java
jar cvf currency.jar CurrencyTickerApplet.class
HTML-страница:
<!-- Курсы валют для казахстанского банковского сайта, 1999 год -->
<applet code="CurrencyTickerApplet.class"
archive="currency.jar"
width="480"
height="28"
alt="Требуется Java">
<param name="rates" value="USD/KZT 130.00 EUR/KZT 138.50 RUB/KZT 5.80">
<param name="speed" value="2">
<p><b>Ваш браузер не поддерживает Java.</b>
Курсы: USD 130.00, EUR 138.50, RUB 5.80</p>
</applet>
Интерактивная карта офиса
Другое применение апплетов, популярное в казнете 1999-2000 годов - интерактивная карта с кликабельными районами:
// OfficeMapApplet.java - кликабельная карта с районами Алматы
// Наведи на район - подсветка; кликни - переход на страницу
import java.applet.Applet;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
public class OfficeMapApplet extends Applet
implements MouseListener, MouseMotionListener {
private Image mapImage;
private int hoveredRegion = -1;
// Прямоугольные области районов (x, y, ширина, высота)
private int[][] regions = {
{20, 30, 80, 60}, // Алмалинский район
{110, 30, 80, 60}, // Бостандыкский район
{200, 30, 80, 60}, // Медеуский район
};
private String[] regionNames = {
"Алмалинский", "Бостандыкский", "Медеуский"
};
private String[] regionURLs = {
"almaly.html", "bostandyk.html", "medeu.html"
};
@Override
public void init() {
mapImage = getImage(getCodeBase(), getParameter("map"));
addMouseListener(this);
addMouseMotionListener(this);
setBackground(Color.white);
}
@Override
public void paint(Graphics g) {
if (mapImage != null) {
g.drawImage(mapImage, 0, 0, this);
}
for (int i = 0; i < regions.length; i++) {
if (i == hoveredRegion) {
// Полупрозрачная подсветка через XOR
g.setXORMode(new Color(255, 255, 0));
g.fillRect(regions[i][0], regions[i][1],
regions[i][2], regions[i][3]);
g.setPaintMode();
// Название региона
g.setColor(Color.black);
g.setFont(new Font("SansSerif", Font.BOLD, 11));
g.drawString(regionNames[i],
regions[i][0] + 4, regions[i][1] + 14);
}
}
}
@Override
public void mouseClicked(MouseEvent e) {
int clicked = getRegionAt(e.getX(), e.getY());
if (clicked >= 0) {
try {
getAppletContext().showDocument(
new URL(getDocumentBase(), regionURLs[clicked]),
"_self"
);
} catch (MalformedURLException ex) {
showStatus("Ошибка перехода: " + ex.getMessage());
}
}
}
@Override
public void mouseMoved(MouseEvent e) {
int region = getRegionAt(e.getX(), e.getY());
if (region != hoveredRegion) {
hoveredRegion = region;
repaint();
if (region >= 0) {
showStatus("Район: " + regionNames[region]);
} else {
showStatus("");
}
}
}
private int getRegionAt(int x, int y) {
for (int i = 0; i < regions.length; i++) {
if (x >= regions[i][0] && x <= regions[i][0] + regions[i][2] &&
y >= regions[i][1] && y <= regions[i][1] + regions[i][3]) {
return i;
}
}
return -1;
}
@Override public void mousePressed(MouseEvent e) {}
@Override public void mouseReleased(MouseEvent e) {}
@Override public void mouseEntered(MouseEvent e) {}
@Override public void mouseExited(MouseEvent e) { hoveredRegion = -1; repaint(); }
@Override public void mouseDragged(MouseEvent e) {}
}
Реальность: загрузка и совместимость в казнете
В 1999 году типичный пользователь в Алматы работал с:
| Параметр | Значение |
|---|---|
| Соединение | Dial-up 28-33 кбит/с |
| Браузер | IE 4/5 или Netscape 4 |
| RAM | 32-64 МБ |
| JVM | Microsoft JVM (в IE) или Sun JVM (в Netscape) |
Проблема с двумя JVM была реальной:
// Microsoft JVM (IE4/5) и Sun JVM (Netscape) отличались:
// - Microsoft JVM не поддерживал полный RMI
// - Поведение java.net.URL различалось
// - Thread.sleep() иногда давала неверные результаты в MS JVM
// Решение: тестировать в обоих браузерах, избегать продвинутых API
// Детект JVM в JavaScript для выбора фолбэка:
var isIE = (navigator.appName === "Microsoft Internet Explorer");
// Если IE - убедиться что Microsoft JVM установлен
// Если Netscape - проверить наличие плагина Sun JVM
Размер JAR-файла напрямую влиял на конверсию в казнете:
- 50 КБ JAR - 12-15 секунд загрузки @ 33 кбит/с - приемлемо
- 200 КБ JAR - 50-60 секунд - пользователи уходили
- 500 КБ JAR - 2 минуты+ - никто не ждал
Мы оптимизировали агрессивно: убирали все неиспользуемые классы, сжимали изображения максимально, разбивали апплет на несколько JAR с ленивой загрузкой второстепенных.
Flash выиграл: почему это было неизбежно
К 2000 году Macromedia Flash 4 стал доминировать в нише «интерактивный веб» по нескольким причинам, критично важным именно для казнета:
Меньше размер файлов. Flash-ролик в 20 КБ против JAR в 100 КБ при одинаковом визуальном результате. На slow dial-up это было решающим.
Плагин шёл вместе с браузером. Netscape 4 и IE 5 поставлялись с Flash-плагином предустановленным. JVM нужно было скачивать и устанавливать отдельно - 10 МБ по dial-up, несколько часов.
Инструменты для дизайнеров. Флеш-анимацию мог создать дизайнер без знания Java. В Алматы 1999 года программистов, знавших Java, было очень мало. Дизайнеров со знанием Flash - существенно больше.
Java-апплеты в казнете ушли к 2001-2002 году, не успев стать массовой технологией. Но те, кто писал апплеты в 1999-м, получил бесценный опыт - событийное программирование, работа с потоками, двойная буферизация. Этот опыт напрямую конвертировался в Android-разработку, начавшуюся в 2008 году.