Belajar Membuat Komponen Baru Di Swing

NetBeans Swing GUI Builder boleh dibilang adalah visual editor terbaik untuk Swing. Berkat NetBeans Swing GUI Builder, pengguna dapat merancang GUI berbasis Swing dengan mudah dan cepat seperti pada seri ‘visual’ -nya Microsoft. Masalah yang kemudian muncul adalah kampus lokal kemudian membuat kurikulum pemograman Swing dan menggantinya dengan visual builder. Mereka berusaha mengajarkan Swing dan Java seperti pada teknik Rapid Application Development (RAD) layaknya Visual Basic.

Memilih untuk memakai GUI Builder atau membuat GUI secara kode program adalah selera ‘hidup’ masing-masing developer yang tidak boleh dipaksakan. Tapi, mempelajari komponen Swing tanpa menyentuh arsitektur Swing dan memperlakukan mereka layaknya komponen terbatas seperti di seri ‘visual’ adalah sebuah kesalahan besar. Mengapa demikian? Hal ini karena justru kelebihan utama Swing adalah sifatnya yang modular dan extensible.

Sebagai contoh, saya menemukan keterbatasan JToolBar pada kasus yang saya hadapi. Bila ada banyak icon di JToolBar dan ukuran layar terbatas, maka mereka akan terpotong. Sebagai contoh, pada JToolBar saya, terdapat 9 icon besar. Tapi bila JFrame diperkecil, maka icon yang tidak terlihat akan ‘hilang’ seperti yang ditunjukkan pada gambar berikut ini:

JToolBar akan menyembunyikan icon bila tidak muat

JToolBar akan menyembunyikan icon bila tidak muat

Saya membutuhkan fasilitas scrolling sehingga saya tetap dapat memilih icon yang tidak ditampilkan. Tapi, tidak ada properties di JToolBar yang bisa saya atur untuk keperluan ini! Apakah ini berarti ‘kiamat’? Pada teknologi GUI lain, bila sudah tidak ada properties yang bisa diatur, jawabannya mungkin ‘iya’. Tapi tidak untuk Swing! Pada Swing, saya bisa membuat komponen baru dengan mudah. Saya bisa men-reuse elemen dari sebuah komponen tanpa harus membuat segala sesuatunya dari awal.

Sebagai contoh, saya membuat versi JToolBar yang bisa di-scroll dengan nama ScrollableToolBar yang kode programnya terlihat seperti berikut ini:

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.plaf.basic.BasicArrowButton;
import java.awt.*;
import java.awt.event.*;

public class ScrollableToolBar extends JPanel {

    private JToolBar toolBar;
    private JScrollPane scrollPane;
    private JButton btnLeft;
    private JButton btnRight;

    public ActionLeft actionLeft = new ActionLeft();
    public ActionRight actionRight = new ActionRight();

    public ScrollableToolBar() {
        toolBar = new JToolBar();
        scrollPane = new JScrollPane(toolBar, ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        scrollPane.getViewport().addChangeListener(new DisplayButtonChangeListener());
        btnLeft = new BasicArrowButton(SwingConstants.WEST);
        btnLeft.setAction(actionLeft);
        btnLeft.addMouseListener(new PressButtonMouseAdapter(actionLeft));
        btnRight = new BasicArrowButton(SwingConstants.EAST);
        btnRight.addMouseListener(new PressButtonMouseAdapter(actionRight));

        setLayout(new BorderLayout());
        add(scrollPane, BorderLayout.CENTER);
        add(btnLeft, BorderLayout.LINE_START);
        add(btnRight, BorderLayout.LINE_END);
    }

    public JToolBar getToolBar() {
        return toolBar;
    }

    @Override
    public Component add(Component comp) {
        return toolBar.add(comp);
    }

    public class ActionLeft extends AbstractAction {

        @Override
        public void actionPerformed(ActionEvent e) {
            JScrollBar scrollBar = scrollPane.getHorizontalScrollBar();
            scrollBar.setValue(scrollBar.getValue() - scrollBar.getBlockIncrement());
        }

    }

    public class ActionRight extends AbstractAction {

        @Override
        public void actionPerformed(ActionEvent e) {
            JScrollBar scrollBar = scrollPane.getHorizontalScrollBar();
            scrollBar.setValue(scrollBar.getValue() + scrollBar.getBlockIncrement());
        }

    }

    public class PressButtonMouseAdapter extends MouseAdapter implements ActionListener {

        private Action action;
        private Timer timer;

        public PressButtonMouseAdapter(final Action action) {
            this.action = action;
            timer = new Timer(20, this);
        }

        @Override
        public void mousePressed(MouseEvent e) {
            timer.start();
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            timer.stop();
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            action.actionPerformed(null);
        }
    }

    private class DisplayButtonChangeListener implements ChangeListener {

        @Override
        public void stateChanged(ChangeEvent e) {
            JViewport viewport = scrollPane.getViewport();
            boolean buttonVisible = (toolBar.getWidth() > viewport.getWidth());
            btnLeft.setVisible(buttonVisible);
            btnRight.setVisible(buttonVisible);
        }
    }

}

Sekarang, saya dapat melakukan scrolling pada JToolBar seperti yang diperlihatkan pada gambar berikut ini:

Komponen baru dengan JToolBar yang dapat di-scroll

Komponen baru dengan JToolBar yang dapat di-scroll

ScrollableToolBar diturunkan dari class JPanel sehingga merupakan komponen yang terdiri dari beberapa komponen lainnya. Pada kode program di atas, ScrollableToolBar terdiri atas 2 JButton, sebuah JScrollPane dan sebuah JToolBar yang masing-masing diwakili oleh variabel seperti yang terlihat pada visualisasi berikut ini:

Komposisi dari ScrollableToolBar

Komposisi dari ScrollableToolBar

Saya membutuhkan JScrollPane untuk melakukan scrolling. Tapi, saya tidak ingin memperlihatkan scrollbar. Oleh sebab itu, saya membuat JScrollPane dengan constructor seperti pada:

scrollPane = new JScrollPane(toolBar, 
   ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER, 
   ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

btnLeft dan btnRight adalah sebuah JButton biasa yang memiliki gambar panah. Untuk itu, saya memakai BasicArrowButton yang merupakan komponen yang dipakai oleh JScrollBar. Yup, scrollbar sudah punya tombol panah, saya tidak perlu membuat lagi dari awal!. Disini terlihat bahwa saya bisa me-reuse elemen sebuah komponen dengan mudah. Btw, visual editor biasanya tidak akan menampilkan BasicArrowButton untuk di-drag sehingga komponen seperti ini hanya bisa dipakai melalui kode program! Pada constructor BasicArrowButton, saya menentukan arah tanda panah seperti yang terlihat pada kode program berikut ini:

btnLeft = new BasicArrowButton(SwingConstants.WEST);
btnRight = new BasicArrowButton(SwingConstants.EAST);

Untuk melakukan scrolling pada btnLeft atau btnRight di-klik, saya memanipulasi scrollbar tak terlihat milik JScrollPane seperti yang terlihat pada kode program berikut ini:

public class ActionLeft extends AbstractAction {

  @Override
  public void actionPerformed(ActionEvent e) {
    JScrollBar scrollBar = scrollPane.getHorizontalScrollBar();
    scrollBar.setValue(scrollBar.getValue() - scrollBar.getBlockIncrement());
  }

}

public class ActionRight extends AbstractAction {

   @Override
   public void actionPerformed(ActionEvent e) {
     JScrollBar scrollBar = scrollPane.getHorizontalScrollBar();
     scrollBar.setValue(scrollBar.getValue() + scrollBar.getBlockIncrement());
   }

}

Saya tidak perlu khawatir bila nilai yang berikan melalui JScrollBar.setValue() melewati batas yang diperbolehkan karena setValue() akan memakai nilai maksimal atau nilai minimum bila ada nilai yang melewati batas yang diperbolehkan.

Action di atas didaftarkan agar dikerjakan bila JButton di-klik. Pada saat melakukan scrolling, pengguna cenderung tidak hanya men-klik, melainkan menahan tombol sehingga proses scrolling dapat berlangsung terus menerus. Oleh sebab itu, saya memakai MouseAdapter untuk menjalankan sebuah timer yang aktif bila pengguna menahan tombol (pada handler mousePressed) dan tidak aktif setelah pengguna melepaskan tombol (pada handler mouseReleased). Hal ini terlihat pada cuplikan kode program berikut ini:

public class PressButtonMouseAdapter extends MouseAdapter implements ActionListener {

  private Action action;
  private Timer timer;

  public PressButtonMouseAdapter(final Action action) {
    this.action = action;
    timer = new Timer(20, this);
  }

  @Override
  public void mousePressed(MouseEvent e) {
    timer.start();
  }

  @Override
  public void mouseReleased(MouseEvent e) {
    timer.stop();
  }

  @Override
  public void actionPerformed(ActionEvent e) {
    action.actionPerformed(null);
  }
}

Class javax.swing.Timer (jangan tertukar dengan java.util.Timer!) adalah sebuah class yang sangat berguna untuk mengerjakan sebuah aksi pada GUI secara periodik. Class ini tidak terlihat di NetBeans Swing GUI Editor sehingga mahasiswa yang mempelajari Swing sebagai komponen drag-n-drop cenderung mengabaikannya. Ini adalah salah satu alasan mengapa saya selalu menyarankan mahasiswa mulai membuat GUI berbasis Swing melalui kode program terlebih dahulu. Setelah akrab dengan Swing, mereka boleh menyentuh Swing GUI Editor (analogi yang sama pada dunia web: mahasiswa belajar menulis HTML dan CSS terlebih dahulu baru boleh menyentuh GUI designer seperti Adobe Dreamweaver).

Btw, salah satu cara jitu untuk mempelajari Swing adalah dengan mempelajari kode program untuk komponen Swing itu sendiri. Yup, source untuk seluruh komponen Swing (dan juga API Java lainnya) bisa dibaca secara bebas!. Sebagai contoh, saya meniru cara memakai Timer saat tombol mouse ditahan dengan mempelajari kode program untuk JScrollBar itu sendiri yang bisa ditemui pada class javax.swing.plaf.basic.BasicScrollBarUI. Ini adalah cuplikan kode programnya:

...

/**
 * Listener for cursor keys.
 */
protected class ArrowButtonListener extends MouseAdapter
{
   // Because we are handling both mousePressed and Actions
   // we need to make sure we don't fire under both conditions.
   // (keyfocus on scrollbars causes action without mousePress
   boolean handledEvent;

   public void mousePressed(MouseEvent e)          {
     if(!scrollbar.isEnabled()) { return; }
       // not an unmodified left mouse button
       //if(e.getModifiers() != InputEvent.BUTTON1_MASK) {return; }
       if( ! SwingUtilities.isLeftMouseButton(e)) { return; }
       int direction = (e.getSource() == incrButton) ? 1 : -1;

       scrollByUnit(direction);
       scrollTimer.stop();
       scrollListener.setDirection(direction);
       scrollListener.setScrollByBlock(false);
       scrollTimer.start();

       handledEvent = true;
       if (!scrollbar.hasFocus() && scrollbar.isRequestFocusEnabled()) {
          scrollbar.requestFocus();
       }
   }

   public void mouseReleased(MouseEvent e)         {
       scrollTimer.stop();
       handledEvent = false;
       scrollbar.setValueIsAdjusting(false);
   }
}
...

Sebagai langkah terakhir, saya ingin tombol btnLeft dan btnRight hanya muncul bila wilayah yang ada tidak cukup. Bila masih ada banyak tempat kosong, maka kedua tombol tersebut tidak perlu muncul. Untuk itu, saya perlu berinteraksi dengan JViewPort yang dimiliki oleh JScrollPane. Class JViewPort mewakili wilayah yang terlihat di JScrollPane. Saya bisa mengerjakan sebuah aksi setiap kali wilayah ini berubah dengan membuat kode program seperti berikut ini:

scrollPane.getViewport().addChangeListener(
   new DisplayButtonChangeListener());

Pada ChangeListener yang ada, saya akan membandingkan lebar JViewPort dengan lebar JToolBar. Bila JViewPort masih cukup menampung seluruh JToolBar, maka saya menyembunyikan btnLeft dan btnRight seperti yang terlihat pada cuplikan kode program berikut ini:

private class DisplayButtonChangeListener implements ChangeListener {

   @Override
   public void stateChanged(ChangeEvent e) {
     JViewport viewport = scrollPane.getViewport();
     boolean buttonVisible = (toolBar.getWidth() > viewport.getWidth());
     btnLeft.setVisible(buttonVisible);
     btnRight.setVisible(buttonVisible);
   }

}

Sampai disini, saya sudah memperoleh sebuah komponen baru sesuai dengan keinginan saya, cukup dengan membuat sebuah class baru. Sifat reusable dan extensible merupakan kelebihan Swing yang membuatnya jauh lebih tangguh dari sekedar komponen visual biasa yang di-drag n drop pada GUI editor.

Iklan

Mencari Kebocoran Memori Di Program Java Dengan JVisualVM dan Eclipse MemoryAnalyzer

Kebocoran memori pada sebuah program terjadi bila programmer memakai memori untuk menampung data tetapi tidak melepaskan wilayah memori tersebut walaupun data sudah tidak dipakai lagi. Bila hal ini terjadi terus menerus, sebanyak apapun memori yang tersedia, pada akhirnya akan ‘habis’. Istilah ‘habis’ yang dimaksud adalah data di memori secara logika sesungguhnya sudah tidak dipakai lagi dan boleh digunakan untuk menampung data baru, tetapi yang terjadi adalah wilayah memori tersebut tetap dianggap sedang dipakai.

Pada bahasa C, pengguna dapat meminta wilayah memori untuk dipakai dengan menggunakan malloc(). Setelah tidak dibutuhkan lagi, wilayah memori tersebut dilepaskan dengan menggunakan free(). Pada Java, programmer perlu mengalokasikan penggunaan memori dengan new tetapi tidak perlu melepaskan wilayah memori tersebut secara manual. Fasilitas garbage collector di Java akan mencari object yang sudah tidak pakai dan mem-‘buang’-nya secara otomatis. Garbage collector selalu bekerja di balik layar pada saat program Java dijalankan.

Bila sudah ada garbage collector, apakah itu berarti kebocoran memori tidak akan terjadi di program yang dibuat dengan Java? Tidak juga! Garbage collector hanya akan mem-‘buang’ object yang benar-benar tidak dirujuk oleh object lainnya lagi. Jadi, walaupun sebuah object sudah tidak dipakai lagi, tetapi bila masih ada object lain yang menyimpan referensinya, maka garbage collector tidak akan mem-‘buang’ object tersebut.

Sebagai contoh, saya memantau sebuah program Java yang saya buat dengan menggunakan JVisualVM. Ini adalah tool bawaan JDK yang dapat dijumpai di folder seperti C:\Program Files\java\jdk1.7\bin\jvisualvm.exe. Sebagai informasi, di folder ini juga masih ada banyak tool berguna lainnya yang tidak memiliki shortcut di Start Menu. Setelah menjalankan JVisualVM, saya dapat melihat semua program Java yang sedang berjalan di komputer lokal di window Applications . Saya kemudian men-double click pada program Java yang hendak saya pantau. Setelah itu, saya men-klik pada tab Monitor. Pada bagian Heap, saya menemukan tampilan seperti pada gambar berikut ini:

Grafik menunjukkan ada kebocoran memori

Grafik menunjukkan ada kebocoran memori

Grafik di atas memperlihatkan memori di heap secara terus menerus meningkat hingga mencapai batas maksimal yang diperbolehkan. Ini akan menyebabkan program Java berjalan sangat lambat. Bahkan setelah saya men-klik tombol Perform GC, grafik tidak menunjukkan penurunan wilayah heap yang terpakai. Saya yakin telah terjadi kebocoran memori yang sangat besar karena program saya saat ini tidak sedang melakukan apa-apa. Bagaimana mungkin sebuah program yang sedang santai ‘memakan’ memori sebesar itu?

Lalu bagaimana cara mengetahui kode program mana yang menyebabkan terjadinya kebocoran memori? Saya akan mulai melakukan troubleshooting dengan men-klik tombol Heap Dump. Pada window Applications, saya men-klik kanan heap dump yang dihasilkan dan memilih Save As untuk menyimpannya sebagai sebuah file.

Saya akan menggunakan Eclipse MemoryAnalyzer (MAT) untuk melakukan analisa secara otomatis. MAT dapat di-download sebagai sebuah aplikasi standalone (Eclipse RCP) di http://eclipse.org/mat/downloads.php. Setelah men-extract file zip yang di-download, saya men-double click file MemoryAnalyzer.exe untuk menjalankan MAT. Setelah program dijalankan, saya memilih menu File, Open Heap Dump… untuk membuka file heap dump yang sudah saya buat sebelumnya. Pada dialog Getting Started, saya memilih Leak Suspects Report dan men-klik tombol Finish.

Setelah proses analisa selesai, saya memperoleh laporan seperti yang terlihat pada gambar berikut ini:

Hasil analisa Eclipse MemoryAnalyzer

Hasil analisa Eclipse MemoryAnalyzer

Laporan tersebut memperlihatkan bahwa ada 31 object yang merupakan instance dari class PrintPreviewPane yang memakai hingga 67% memori di heap. Padahal seluruh JPanel tersebut sudah tidak dipakai lagi karena saya sudah menutup dialog-nya sejak lama. Saya sudah menemukan ‘tersangka’! Langkah selanjutnya adalah mengetahui kronologi kejadian agar saya bisa memperbaiki kebocoran ini. Untuk itu, saya bisa melihat pada bagian Reference Pattern seperti pada gambar berikut ini:

Output pada Reference pattern

Output pada Reference pattern

Ternyata PrintPreviewPane adalah sebuah JPanel yang ditampilkan melalui JDialog. Bila seandainya setiap JDialog yang ada tersebut dibersihkan oleh garbage collector, program bisa memperoleh memori sebesar 144 MB untuk dipakai ulang. Seluruh child dan data yang terkandung di dalam PrintPreviewPane tidak dihapus sama sekali oleh garbage collector.

Mengapa garbage collector tidak membuang JDialog yang sudah tidak ditampilkan dari memori? Ini adalah kesalahan klasik dimana saya lupa memanggil dispose() dari JDialog setelah menutup dialog tersebut. Pada Swing, hampir seluruh top-level container seperti JFrame, JDialog, JWindow dan sebagainya harus di-dispose() bila sudah tidak dipakai lagi. Loh, bukankah seharusnya garbage collector bekerja secara pintar? Masalahnya, top-level container memakai resource spesifik milik sistem operasi yang berhubungan dengan GUI. Selain itu, top-level container juga bisa ditampilkan ulang setelah ditutup, misalnya dengan setVisible(true). Hanya dengan dispose() baru seluruh alokasi sumber daya milik sistem operasi tersebut akan benar-benar dilepaskan.

Saya pun segera memperbaiki kesalahan pada kode program Java yang saya buat, misalnya dengan menambahkan kode program seperti:

d.dispose();
d = null;

Sekarang, bila saya memakai JVisualVM untuk memantau aktifitas memori, saya akan menemukan hasil seperti pada gambar berikut ini:

Aktifitas memori setelah kebocoran 'besar' diperbaiki

Aktifitas memori setelah kebocoran ‘besar’ diperbaiki

Terlihat bahwa garbage collector berhasil menghapus object yang tidak dibutuhkan dimana grafik penggunaan memori terlihat menurun. Kebocoran memori mungkin saja masih terjadi, tetapi tidak ada lagi kebocoran ‘besar’ yang menyebabkan program menjadi sangat lambat akibat kehabisan memori.

Memakai Java Web Start Pada Aplikasi Yang Dibuat Dengan Griffon

Pada aplikasi web, developer hanya perlu melakukan perubahan pada server dan pengguna akan segera memperoleh halaman terbaru. Lalu bagaimana dengan aplikasi desktop? Saya harus men-install aplikasi secara manual satu per satu pada seluruh komputer pengguna. Bila mereka tersebar dalam beberapa lantai berbeda, saya terpaksa harus naik turun tangga. Ini adalah sesuatu yang melelahkan. Beruntungnya, bila memakai Java, saya dapat menggunakan Java Web Start untuk mempermudah distribusi aplikasi desktop yang dibuat dengan Java SE. Griffon membuatnya menjadi lebih mudah lagi.

Langkah pertama yang saya lakukan adalah mengubah isi file griffon-app/conf/BuildConfig.groovy menjadi seperti berikut ini:

environments {
  ...
  production {
    ...
    signingKey {
      params {
         storepass = 'solidsnake'
         keypass = 'solidsnake'
         lazy = false
      }
    }
    ...
  }
  ...
}

Griffon secara otomatis akan melakukan signing pada JAR untuk didistribusikan melalui Java Web Start. Dengan melakukan signing pada JAR, saya menyatakan bahwa file JAR yang saya distribusikan dapat dipercaya sehingga bisa memperoleh hak akses yang lebih banyak seperti menulis file di komputer client. Karena saya tidak membeli private key dari Certificate Authority (seperti VeriSign), maka Griffon akan memakai self-signed certificate. Ini adalah sertifikat digital yang hanya dipakai untuk keperluan percobaan dan masa berlakunya terbatas. Pengguna yang menjalankan aplikasi yang menggunakan self-signed certificate melalui Java Web Start akan memperoleh peringatan keamanan.

Selain itu, saya juga perlu mengubah nilai codebase agar sesuai dengan lokasi URL yang menampung hasil distribusi, misalnya:

environments {
  ...
  production {
    ...
    griffon {
      ...
      webstart {
        codebase = 'http://myserver.com/latihan'
      }
      ...
    }
    ...
  }
  ...
}

Selanjutnya, untuk menghasilkan file yang dapat didistribusikan melalui Java Web Start, saya memberikan perintah berikut ini:

C:\proyek> griffon package webstart

Perintah di atas akan menghasilkan file pada lokasi dist/webstart. Setiap file JAR akan memiliki versi yang telah di-compress dengan tool pack200 dimana hasilnya memiliki ekstensi .pack.gz. File JAR yang telah di-compress dapat di-download lebih cepat oleh pengguna. Griffon juga telah memberikan nilai true untuk jnlp.packEnabled secara otomatis.

File yang paling penting disini adalah application.jnlp. File dalam format Java Network Launch Protocol (JNLP) ini adalah file yang perlu dikerjakan oleh pengguna. Saya melakukan sedikit perubahan pada file ini, seperti mengubah versi Java yang dibutuhkan dari 1.5+ menjadi 1.7. Selain itu, saya menghilangkan komentar pada bagian berikut ini:

<shortcut online="true">
   <desktop/>
</shortcut>
<offline-allowed/>

Tujuannya adalah agar Java Web Start menghasilkan file shortcut di desktop sehingga pengguna nantinya bisa menjalankan aplikasi langsung tanpa harus membuka browser terlebih dahulu.

Langkah berikutnya yang perlu saya lakukan adalah meletakkan file-file yang ada di dist/webstart pada sebuah web server yang dapat diakses oleh seluruh komputer pengguna. Sebagai contoh, saya akan memakai IIS bawaan Windows 7 sebagai web server. Saya men-copy isi file yang dihasilkan Griffon ke lokasi C:\inetpub\webstart. Setelah itu, saya membuat sebuah application baru dengan men-klik kanan sebuah site dan memilih menu Add Application…. Saya kemudian mengisi dialog yang muncul seperti pada gambar berikut ini:

Membuat application baru di IIS

Membuat application baru di IIS

Selain itu, saya juga memilih MIME Types dan menambahkan MIME untuk JNLP seperti berikut ini:

Mendaftarkan MIME untuk JNLP

Mendaftarkan MIME untuk JNLP

Agar lebih mudah dipakai, saya mengubah iisstart.htm menjadi seperti berikut ini:

<html>
<body>
<a href='latihan/application.jnlp'>Klik disini</a> untuk menjalankan aplikasi terbaru kita.
</body>
</html>

Sekarang, bila saya membuka URL di web server seperti http://myserver.com, saya dapat menjalankan aplikasi melalui Java Web Start seperti yang terlihat pada gambar berikut ini:

Menjalankan Aplikasi Dari Browser

Menjalankan Aplikasi Dari Browser

Bila saya memilih Ok, maka Java Web Start Launcher akan men-download file yang dibutuhkan dari server dan menyimpannya sebagai cache di komputer pengguna. Karena saya memakai self-signed certificate yang tidak aman, akan muncul pesan peringatan seperti pada gambar berikut ini:

Pesan Peringatan

Pesan Peringatan

Saya memberi tanda centang pada I accept the risk dan men-klik tombol Run. Aplikasi akan dijalankan. Java Web Start Launcher juga akan membuat sebuah shortcut di Desktop sehingga lain kali, saya dapat menjalankan aplikasi ini tanpa harus membuka browser terlebih dahulu.

Apa yang terjadi bila web server dimatikan? Apakah aplikasi tetap dapat dijalankan? Yup, tetap bisa! Hal ini karena Java Web Start menyimpan aplikasi secara lokal. Untuk membuktikannya, saya membuka Control Panel, Java. Pada bagian General, saya men-klik tombol View…. Saya akan memperoleh tampilan seperti pada gambar berikut ini:

Melihat Aplikasi Yang Ada Di Cache

Melihat Aplikasi Yang Ada Di Cache

Pada dialog di atas, saya juga bisa menghapus cache yang sudah ada, menjalankan aplikasi, atau membuat kembali shortcut yang terhapus.

Lalu, bagaimana bila saya melakukan perubahan pada kode program dan ingin perbaharuan dilakukan pada seluruh klien yang sudah memiliki cache aplikasi di komputer lokal mereka?

Saya tidak perlu khawatir karena setiap kali aplikasi dijalankan, Java Web Start akan men-download ulang seluruh JAR dan memakai JAR yang terbaru. Bila sedang tidak dapat terhubung ke server, maka apa yang ada di cache akan dipakai (dengan catatan konfigurasi di file JNLP membolehkan aplikasi bekerja secara offline).

Untuk menghemat waktu download, JNLP juga memiliki fasilitas version-based download protocol. Dengan version-based download protocol, Java Web Start tidak perlu men-download seluruh JAR melainkan hanya JAR yang versinya berubah atau tidak ada di cache. Namun, sepertinya Griffon belum menghasilkan nama file JAR dan isi JNLP yang mendukung version-based download protocol.

Mencetak Dengan Java Print Service API

Saya mendapat pertanyaan dari seorang teman yang bekerja sebagai freelancer:  bagaimana cara mencetak ke printer dengan nama tertentu di Java?   Pada kasusnya, terdapat tiga printer dengan nama berbeda dimana masing-masing printer memiliki ‘tugas‘-nya sendiri. Bagaimana caranya agar proses percetakan ke masing-masing printer berlangsung otomatis (secara programatis) ke printer yang telah ditentukan, tanpa memunculkan dialog percetakan (sehingga pengguna tidak perlu memilih printer secara manual)?

Seperti yang diketahui, cara mudah untuk mencetak ke printer di platform Java adalah dengan memakai Java 2D Printing API.   Developer bahkan bisa mencetak isi sebuah komponen di layar dengan memanggil method print() atau printAll().   Akan tetapi, pada kasus tertentu, developer menginginkan kendali yang lebih jauh lagi.   Untuk itu, saya dapat menggunakan Java Print Service (JPS) API.

Pada JPS, sebuah instance dari PrintService mewakili sebuah printer.   Untuk mendapatkan PrintService yang ada, saya dapat memakai class PrintServiceLookup.   Format yang didukung oleh sebuah PrintService diwakili oleh class DocFlavor.   Percetakan hanya dapat dilakukan ke printer yang mendukung DocFlavor dari dokumen yang akan dicetak.

Sebagai contoh, berikut ini adalah kode program yang menampilkan nama dan DocFlavor yang didukung oleh printer default.  Pada platform Windows, printer default adalah printer yang memiliki tanda centang bila dilihat di Control Panel, Devices and Printers:

import javax.print.DocFlavor;
import javax.print.PrintService;
import javax.print.PrintServiceLookup;

public class Main {

    public static void main(String[] args) {
        System.out.println("Menampilkan informasi untuk printer default:");
        PrintService service = PrintServiceLookup.lookupDefaultPrintService();
        System.out.printf("Nama printer: %s\n", service.getName());
        System.out.println("Format yang didukung:");
        for (DocFlavor format: service.getSupportedDocFlavors()) {
            System.out.println(format);
        }
    }
}

Berikut ini adalah contoh dari output kode program di atas:

Menampilkan informasi untuk printer default:
Nama printer: NAMA_PRINTER_1
Format yang didukung:
image/gif; class="[B"
image/gif; class="java.io.InputStream"
image/gif; class="java.net.URL"
image/jpeg; class="[B"
image/jpeg; class="java.io.InputStream"
image/jpeg; class="java.net.URL"
image/png; class="[B"
image/png; class="java.io.InputStream"
image/png; class="java.net.URL"
application/x-java-jvm-local-objectref; class="java.awt.print.Pageable"
application/x-java-jvm-local-objectref; class="java.awt.print.Printable"
application/octet-stream; class="[B"
application/octet-stream; class="java.net.URL"
application/octet-stream; class="java.io.InputStream"

MIME application/x-java-jvm-local-objectref adalah penanda untuk service-formatted print data dimana JPS akan menentukan sendiri format yang akan dicetak.   Bila memakai MIME ini, maka developer harus melewatkan object yang men-implementasi-kan class/interface yang tertera.   Sebagai contoh, pada output di atas, saya dapat melewatkan sebuah instance yang meng-implementasi-kan salah satu dari interface Pageable atau Printable.

MIME application/octet-stream adalah penanda untuk autosense print data.   Bila memakai MIME jenis ini, maka printer akan menentukan sendiri format dokumen yang akan dicetak.   Nilai yang ditunjukkan pada ‘class=’ adalah class yang dapat dipakai untuk mewakili data. Sebagai contoh, bila memakai application/octet-stream, berdasarkan output di atas, saya dapat melewatkan data salah satu bentuk berikut ini: byte array, instance dari java.net.URL atau instance dari java.io.InputStream.

Pada kode program sebelumnya, saya mengambil informasi dari printer default.   Bagaimana bila saya ingin mengambil informasi dari seluruh printer yang ter-install di komputer ini?   Saya dapat menggunakan kode program seperti berikut ini:

import javax.print.DocFlavor;
import javax.print.PrintService;
import javax.print.PrintServiceLookup;

public class Main {

    public static void main(String[] args) {
        System.out.println("\nMenampilkan informasi untuk seluruh printer:");
        PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
        for (PrintService service: services) {
            System.out.printf("\nNama printer: %s\n", service.getName());
            System.out.println("Format yang didukung:");
            for (DocFlavor format: service.getSupportedDocFlavors()) {
                System.out.println(format);
            }
        }
    }
}

Berikut ini adalah contoh output dari kode program di atas:

Menampilkan informasi untuk seluruh printer:

Nama printer: NAMA_PRINTER_2
Format yang didukung:
image/gif; class="[B"
image/gif; class="java.io.InputStream"
image/gif; class="java.net.URL"
image/jpeg; class="[B"
image/jpeg; class="java.io.InputStream"
image/jpeg; class="java.net.URL"
image/png; class="[B"
image/png; class="java.io.InputStream"
image/png; class="java.net.URL"
application/x-java-jvm-local-objectref; class="java.awt.print.Pageable"
application/x-java-jvm-local-objectref; class="java.awt.print.Printable"
application/octet-stream; class="[B"
application/octet-stream; class="java.net.URL"
application/octet-stream; class="java.io.InputStream"

Nama printer: NAMA_PRINTER_3
Format yang didukung:
image/gif; class="[B"
image/gif; class="java.io.InputStream"
image/gif; class="java.net.URL"
image/jpeg; class="[B"
image/jpeg; class="java.io.InputStream"
image/jpeg; class="java.net.URL"
image/png; class="[B"
image/png; class="java.io.InputStream"
image/png; class="java.net.URL"
application/x-java-jvm-local-objectref; class="java.awt.print.Pageable"
application/x-java-jvm-local-objectref; class="java.awt.print.Printable"
application/octet-stream; class="[B"
application/octet-stream; class="java.net.URL"
application/octet-stream; class="java.io.InputStream"

Nama printer: NAMA_PRINTER_1
Format yang didukung:
image/gif; class="[B"
image/gif; class="java.io.InputStream"
image/gif; class="java.net.URL"
image/jpeg; class="[B"
image/jpeg; class="java.io.InputStream"
image/jpeg; class="java.net.URL"
image/png; class="[B"
image/png; class="java.io.InputStream"
image/png; class="java.net.URL"
application/x-java-jvm-local-objectref; class="java.awt.print.Pageable"
application/x-java-jvm-local-objectref; class="java.awt.print.Printable"
application/octet-stream; class="[B"
application/octet-stream; class="java.net.URL"
application/octet-stream; class="java.io.InputStream"

Bagaimana bila saya hanya ingin mencari printer dengan nama tertentu saja?   Saya dapat melewatkan sebuah AttributeSet pada saat memanggil lookupPrinterServices() seperti yang terlihat pada kode program berikut ini:

import javax.print.DocFlavor;
import javax.print.PrintService;
import javax.print.PrintServiceLookup;
import javax.print.attribute.HashAttributeSet;
import javax.print.attribute.standard.PrinterName;

public class Main {

    public static void main(String[] args) {

        HashAttributeSet attributes = new HashAttributeSet();
        attributes.add(new PrinterName("NAMA_PRINTER_1", null));

        System.out.println("\nMenampilkan printer:");
        PrintService[] services = PrintServiceLookup.lookupPrintServices(null, attributes);
        for (PrintService service: services) {
            System.out.printf("\nNama printer: %s\n", service.getName());
            System.out.println("Format yang didukung:");
            for (DocFlavor format: service.getSupportedDocFlavors()) {
                System.out.println(format);
            }
        }

    }
}

Setelah mendapatkan PrintService yang diinginkan, bagaimana cara mencetak ke printer tersebut?   Untuk mencetak saya harus menghasilkan sebuah DocPrintJob dari PrintService.   Object DocPrintJob ini dapat dipakai untuk mencetak dokumen yang diwakili dengan sebuah Doc.   Proses mencetak akan dimulai setelah saya memanggil method print() dari DocPrintJob tersebut.

Saat saya mencoba mencetak dengan mengirim data langsung ke printer (auto sense),  dimana saya mengirimkan deretan byte karakter ASCII,  saya memperoleh pesan kesalahan ‘Error – printing’ di tampilan queue percetakan Windows.   Hal ini karena saya mencetak ke sebuah printer deskjet ekonomis yang memakai bahasa printer HP Lightweight Imaging Device Interface Language (LIDIL).   Bahasa printer ini sering dipakai oleh printer Hewlett-Packard (HP) ekonomis yang tidak mendukung bahasa Printer Command Language (PCL).   Tidak seperti PCL yang menerima input berupa tulisan teks, LIDIL membutuhkan deretan byte khusus untuk menggambar titik (raster).   LIDIL tidak mendukung perintah berupa teks ASCII.   Oleh sebab itu, saat saya mengirim tulisan teks secara langsung ke printer, saya akan memperoleh pesan ‘Error – printing’ dari printer karena perintah tersebut bukan perintah LIDIL yang valid.

Cara yang lebih aman adalah mencetak tulisan dengan menggunakan service-formatted print data:

import ...

public class Main {

    public static void main(String[] args) {

        HashAttributeSet attributes = new HashAttributeSet();
        attributes.add(new PrinterName("NAMA_PRINTER_1", null));
        DocFlavor format = DocFlavor.SERVICE_FORMATTED.PRINTABLE;
        PrintService[] services = PrintServiceLookup.lookupPrintServices(format, attributes);
        if (services.length > 0) {
            Doc doc = new SimpleDoc(new Printable() {

                @Override
                public int print(Graphics g, PageFormat pageFormat, int pageIndex) throws PrinterException {
                    if (pageIndex==0) {
                        int x = (int) pageFormat.getImageableX();
                        int y = (int) pageFormat.getImageableY();
                        int width = (int) pageFormat.getImageableWidth();
                        int height = (int) pageFormat.getImageableHeight();
                        g.drawRect(x, y, width, height);
                        g.drawString("Selamat datang di TheSolidSnake", x+5, y+20);
                        return PAGE_EXISTS;
                    } else {
                        return NO_SUCH_PAGE;
                    }
                }

            }, format, null);
            try {
                services[0].createPrintJob().print(doc, null);
            } catch (PrintException ex) {
                ex.printStackTrace();
            }
        } else {
            System.out.println("Printer tidak ditemukan!");
        }
    }
}

Bila kode program di atas dijalankan, printer dengan nama “NAMA_PRINTER_1” akan mencetak kertas dengan border dan tulisan ‘Selamat datang di TheSolidSnake’.   Percetakan akan langsung dimulai pada saat itu juga tanpa memunculkan kotak dialog.

Saya juga dapat mencetak komponen GUI dengan cara seperti ini.   Sebagai contoh, kode program berikut ini akan mencetak isi dari JTextArea ke printer dengan nama “NAMA_PRINTER_1”:

import ...

public class Main extends JFrame implements ActionListener {

    private JTextArea txtInput;
    private JButton btnCetak;

    public Main() {
        super("Latihan Mencetak");

        txtInput = new JTextArea();
        btnCetak = new JButton("Cetak Ke Printer Dengan Nama 'NAMA_PRINTER_1'");
        btnCetak.addActionListener(this);

        setLayout(new BorderLayout());
        add(new JScrollPane(txtInput), BorderLayout.CENTER);
        add(btnCetak, BorderLayout.PAGE_END);
    }

    public static void main(String[] args) {

        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                JFrame f = new Main();
                f.pack();
                f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                f.setVisible(true);
            }

        });
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        HashAttributeSet attributes = new HashAttributeSet();
        attributes.add(new PrinterName("NAMA_PRINTER_1", null));
        DocFlavor format = DocFlavor.SERVICE_FORMATTED.PRINTABLE;
        PrintService[] services = PrintServiceLookup.lookupPrintServices(format, attributes);
        if (services.length > 0) {
            try {
                txtInput.print(null, null, false, services[0], null, false);
            } catch (PrinterException ex) {
                ex.printStackTrace();
            }
        } else {
            System.out.println("Printer tidak ditemukan!");
        }

    }
}

Tampilan kode program di atas akan terlihat seperti pada gambar berikut ini:

Tampilan Program

Tampilan Program

Memahami Penggunaan Memori Di Java

Seorang pembaca bertanya pada saya mengapa jumlah memori yang dipakai oleh aplikasi Java-nya besar sekali.  Ia menjelaskan bahwa ia mengetahui hal ini dengan memakai task manager di sistem operasi Windows.  Sebagai contoh, saya menjalankan sebuah aplikasi Griffon yang memakai plugin simple-jpa, mensimulasikan transaksi faktur dengan data real, lalu membuka task manager:

Tampilan Sebuah Program Java Di Task Manager Windows

Tampilan Sebuah Program Java Di Task Manager Windows

Untuk menjawab pertanyaan ini, saya ingin mengingatkan kembali mengenai Java Virtual Machine (JVM).  Tanpa JVM, program Java (dalam bentuk JAR) tidak akan bisa dijalankan.  Lalu apa itu JVM?  JVM adalah sebuah virtual machine!  Maksudnya?  Coba lihat VirtualBox, Microsoft Virtual PC, VMware , dan sebagainya!  Mereka adalah virtual machine yang mensimulasikan sebuah mesin sehingga pengguna Windows dapat menjalankan Linux atau MacOS di komputer mereka.  Begitu juga sebaliknya, dengan virtual machine, pengguna Mac dapat menjalankan sistem operasi Windows.  JVM bukanlah sebuah virtual machine untuk mensimulasikan OS, tapi konsep dasarnya tidak jauh berbeda, sehingga memungkinkan sebuah program Java dijalankan di berbagai platform berbeda.  JVM adalah virtual machine yang khusus menjalankan bytecode yang merupakan hasil kompilasi dari bahasa pemograman Java, Groovy, Scala, JRuby, Jython, dan sebagainya.

Itu sebabnya pada aplikasi Griffon yang saya jalankan, yang terlihat ada sebuah proses javaw.exe.  Ini adalah JVM, bukan program saya dalam bentuk yang dalam jar.  Dengan demikian, akan tidak fair bila membandingkan sebuah JVM dengan sebuah kode program native (.exe).  Program saya belum tentu memakai memori sebanyak itu!

Sama seperti pengguna VirtualBox atau VMware dapat menkonfigurasi jumlah memori yang dipakai, JVM juga memungkinkan konfigurasi memori.  Tapi sebelumnya, lebih baik saya melihat terlebih dahulu penggunaan memori program saya yang sesungguhnya.  Untuk itu, saya membuka lokasi direktori berikut ini di Windows Explorer: C:\Program Files\Java\jdk1.7.0_21\bin.  Di folder ini saya akan menemukan sebuah program bernama jvisualvm.exe.  Saya segera menjalankan program tersebut.  Java VisualVM adalah program bawaan JDK yang memungkinkan developer untuk melihat penggunaan memori aplikasi Java secara detail.  Pada Local, terdapat daftar virtual machine Java yang sedang berjalan termasuk aplikasi saya.   Saya segera men-double click nama aplikasi saya untuk mulai.

Pada tab Monitor, saya dapat melihat penggunaan CPU dan Heap serta PermGen seperti yang terlihat pada gambar berikut ini:

Tampilan monitor CPU dan Memori

Tampilan monitor CPU dan Memori

Gambar di atas adalah aplikasi yang sudah berjalan 20 menit dalam memproses data transaksi invoice tanpa henti.  Memori Heap adalah memori sementara, misalnya nilai variabel lokal ditampung disini.  Dari 98 MB, heap yang terpakai baru 63 MB.   Dari konfigurasi saya, terlihat bahwa maksimal memori komputer yang boleh dipakai sebagai heap adalah 268 MB.  Nilai ini adalah nilai yang kecil bila dibandingkan dengan jumlah memori komputer zaman sekarang (rata-rata 2 hingga 4 GB).

Pada gambar di atas juga terlihat bahwa garbage collector sempat bekerja menjelang jam 14:40.  Garbage collector adalah fitur Java VM yang akan mencari dan membuang object-object yang sudah tak terpakai lagi.

Selain heap, di Java VM juga ada PermGen.  Ini adalah bagian memori yang khusus untuk menampung data yang permanen, seperti informasi struktur class termasuk nilai variabel statis.

Tampilan monitor PermGen

Tampilan monitor PermGen

Untuk informasi yang lebih detail, saya dapat men-klik tab Sampler.  Lalu saya men-klik tombol Memory.  Disini saya dapat melihat informasi mengenai object-object yang ditampung di heap, seperti yang terlihat pada gambar berikut:

Tampilan Heap Sampler

Tampilan Heap histogram

Selain itu, saya juga bisa melihat informasi yang sama untuk PermGen:

Tampilan PermGen histogram

Tampilan PermGen histogram

Pertanyaan berikutnya, alokasi heap saat ini adalah 99 MB dan alokasi PermGen saat ini adalah 26 MB.  Ingat bahwa dari memori yang dialokasikan, tidak semunya terpakai!  Bila ditotalkan, harusnya adalah 125 MB.  Tapi tampilan di task manager adalah 146 MB.  Kemana sisanya?  Java VM (JVM) juga butuh memori untuk keperluan dirinya sendiri, misalnya untuk menampung class-class di JAR yang dibaca.

Dengan memakai tools Windows seperti Sysinternals Process Explorer, saya bisa melihat riwayat penggunaan memori JVM seperti pada gambar berikut ini:

Performance Graph dari Process Explorer

Contoh Tampilan Performance Graph dari Process Explorer

Apakah saya bisa melihat secara detail penggunaan memori di Java VM?  Sama seperti saat saya melihat secara detail penggunaan memori di aplikasi saya sebelumnya?  Iya, bisa, dengan menggunakan Windows SDK dan syaratnya harus memiliki debugging symbol Java VM.  Hal ini bukanlah hal yang menarik untuk dilakukan, apalagi dari gambar di atas tidak terlihat ada kebocoran memori.

Kesimpulannya:  Penggunaan memori untuk aplikasi Java memang lebih besar dibandingkan dengan aplikasi native (exe).  Akan tetapi jumlah pemakaiannya tidak terlalu berlebihan dan masih dalam batas kewajaran (ditambah lagi dengan trend hardware zaman sekarang yang mendukung virtualisasi dan cloudisasi), terutama bila dibandingkan dengan kelebihan Java: kemudahan pengembangan dan portabilitas.

Seberapa Jauh Bedanya Program Yang Multithreading?

Satu fitur dari bahasa Java yang sudah ada sejak awal adalah dukungan multi-threading atau concurrency.  Saya masih ingat saat pertama kali mengenal Java, saya sering diberitahu bahwa salah satu kelebihan Java dibanding C++ adalah dukungan concurrency-nya.  Salah satu buktinya adalah Java memiliki keyword synchronized yang dapat dipakai pada method atau blok kode program.  Selain itu, pada Java 5, terdapat Executor Framework untuk melakukan pooling thread yang berguna dalam meningkatkan kinerja concurrency.  Pada Java 7, Fork/Join Framework diperkenalkan untuk memaksimalkan kinerja concurrency pada prosesor multicore.

Concurrency menjadi semakin penting karena saat ini sudah semakin banyak komputer pengguna yang memiliki prosesor multicore.  Produsen prosesor sepertinya mengalami hambatan dalam meningkatkan clock rate CPU.  Sebagai gantinya, mereka menciptakan prosesor yang lebih ‘kencang’ dengan multicore.  Sebagai contoh, seri prosesor Intel Core i5 dan Intel Core i7 memiliki clock rate yang tidak jauh berbeda (maksimal 3.6 GHz), tetapi perbedaan signifikannya terletak pada jumlah core.   Sebagai contoh, Intel Core i5 2500K memiliki 4 core, sementara itu Intel Core i7 2600K yang dijual lebih mahal $101 juga memiliki 4 core tetapi disertai tambahan Intel Hyper-Threading Technology sehingga memiliki 8 virtual core.  Dari sisi clock rate sendiri, Intel Core i7 2600K  3.4 GHz hanya lebih cepat 100 MHz dibanding Intel Core i5 2500K 3.3 GHz.

Pertanyaannya adalah apakah memiliki 8 core selalu jauh lebih cepat dibanding 4 core?  Tidak juga.  Semua kembali lagi ke software yang dijalankan oleh CPU tersebut.  Bila program yang dijalankan hanya memakai 1 thread, maka tidak akan ada penambahan kinerja yang berarti.  Jadi, untuk mendukung prosesor multicore agar bisa bekerja semaksimal mungkin, developer aplikasi desktop harus membuat aplikasi multithreading.

Pada kesempatan ini saya akan mencoba membandingkan perbedaan kinerja antara kode program Java yang single thread dengan aplikasi yang multi thread. Kedua program akan melakukan hal yang sama, yaitu membaca isi dari seluruh tulisan dari blog ini dan menampilkan informasi jumlah kata yang ditemui.  Agar gampang, saya terlebih dahulu menyimpan isi blog ini dalam sebuah file (ukurannya 1.37 MB).

Ini adalah kode program SingleThread.java yang melakukan proses secara single thread:

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class SingleThread {
    public void proses() throws IOException {
        List<Hasilpencarian> listHasil = new ArrayList<>();
        List<String> listBaris = Files.readAllLines(
            Paths.get("C:\\data.txt"),
            Charset.defaultCharset());

        for (String baris: listBaris) {
            for (String kata: baris.split(" ")) {
                if (kata.trim().length()==0) break;
                kata = kata.toLowerCase();
                HasilPencarian hasilPencarian = new HasilPencarian(kata, 1);
                int index = listHasil.indexOf(hasilPencarian);
                if (index >= 0) {
                    listHasil.get(index).tambahJumlah(hasilPencarian);
                } else {
                    listHasil.add(hasilPencarian);
                }
            }
        }

        Collections.sort(listHasil, new Comparator<HasilPencarian>() {
            @Override
            public int compare(HasilPencarian o1, HasilPencarian o2) {
                return o2.getJumlah() - o1.getJumlah();
            }
        });

        for (HasilPencarian hasilPencarian: listHasil) {
            System.out.println(hasilPencarian);
        }

    }

    public static void main(String[] args) {
        try {
            new SingleThread().proses();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

Kode program tersebut membutuhkan sebuah class HasilPencarian.java yang isinya adalah:

import java.util.Objects;

public class HasilPencarian {

    private static final long serialVersionUID = 1L;

    private String kata;
    private Integer jumlah;

    public HasilPencarian(String kata, Integer jumlah) {
        this.kata = kata;
        this.jumlah = jumlah;
    }

    public String getKata() {
        return kata;
    }

    public void setKata(String kata) {
        this.kata = kata;
    }

    public Integer getJumlah() {
        return jumlah;
    }

    public void setJumlah(Integer jumlah) {
        this.jumlah = jumlah;
    }

    public void tambahJumlah(Integer jumlah) {
       this.jumlah+=jumlah;
    }

    public void tambahJumlah(HasilPencarian hasilLain) {
        if (!hasilLain.getKata().equals(getKata())) {
            throw new RuntimeException("Tidak dapat menjumlahkan kata yang berbeda!");
        }
        tambahJumlah(hasilLain.getJumlah());
    }

    @Override
    public int hashCode() {
        int hash = 5;
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final HasilPencarian other = (HasilPencarian) obj;
        if (!Objects.equals(this.kata, other.kata)) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return String.format("%s: %d", getKata(), getJumlah());
    }

}

Kode program SingleThread.java cukup sederhana dan mudah dimengerti.  Ia akan membaca baris dari file, kemudian memecah setiap baris menjadi kata, lalu menghitung jumlah kata yang unik.  Setelah selesai, ia akan mengurutkan berdasarkan jumlah kata terbanyak dan menampilkannya.  Contoh hasil akan terlihat seperti:

yang: 3448
di: 2071
saya: 1783
akan: 1645
dengan: 1513
untuk: 1496
pada: 1357
dan: 1268
adalah: 1057
seperti: 986
...

Terlihat bahwa selama ini kata yang paling sering saya tulis adalah ‘yang’ 🙂  Ok, tapi bukan itu tujuan saya.  Saya ingin memperlihatkan seperti apa kinerja program ini.  Untuk itu, saya akan melakukan profiling dengan menggunakan NetBeans Profiler.  Untuk itu, saya men-klik icon Profile Project yang terilhat seperti pada gambar berikut ini:

Icon ProfileProject Di NetBeans

Icon ProfileProject Di NetBeans

Saya kemudian memilih CPU, Analyze Performance, dan men-klik tombol Run.  Setelah proses profiling selesai, saya dapat melihat hasil snapshot seperti pada gambar berikut ini:

CPU Sampling Pada Aplikasi Versi Single Thread

CPU Sampling Pada Aplikasi Versi Single Thread

Terlihat bahwa dibutuhkan sekitar 24,6 detik untuk mengerjakan program ini (ini adalah penantian yang cukup lama!).  Waktu paling lama habis dipakai untuk mengerjakan method indexOf() milik ArrayList (karena berada dalam looping).

Saya kembali men-klik icon Profile Project.  Kali ini saya memilih Monitor, memberi tanda centang pada Enable threads monitoring dan Sample threads states.  Setelah men-klik Run dan membiarkan proses profiling selesai, saya dapat melihat daftar threads seperti yang terlihat di gambar berikut ini:

Tampilan Thread Pada Aplikasi Single Thread

Tampilan Thread Pada Aplikasi Single Thread

Disini terlihat bahwa thread pada aplikasi saya hanya satu, yaitu main.  Sisanya adalah thread milik JVM yang selalu ada, misalnya untuk keperluan garbage collector.

Sekarang, saya akan membuat program versi multithread yang memakai Fork/Join Framework dari Java 7.  Kode program ini tidak akan bisa dijalankan pada Java versi sebelumnya.

Saya akan membuat class MultiThread.java yang isinya adalah:

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ForkJoinPool;

public class MultiThread {

    public void proses() throws IOException {

        List<String> listBaris = Files.readAllLines(
            Paths.get("C:\\data.txt"),
            Charset.defaultCharset());

        ForkJoinPool pool = new ForkJoinPool();

        TaskPemisah taskPemisah = new TaskPemisah(listBaris, 0, listBaris.size()-1);
        List<HasilPencarian> listHasil = pool.invoke(taskPemisah);

        Collections.sort(listHasil, new Comparator<HasilPencarian>() {
            @Override
            public int compare(HasilPencarian o1, HasilPencarian o2) {
                return o1.getKata().compareTo(o2.getKata());
            }
        });

        TaskPenghitung taskPenghitung = new TaskPenghitung(listHasil, 0, listHasil.size()-1);
        listHasil = pool.invoke(taskPenghitung);
        pool.shutdown();

        Collections.sort(listHasil, new Comparator<HasilPencarian>() {

            @Override
            public int compare(HasilPencarian o1, HasilPencarian o2) {
                return o2.getJumlah() - o1.getJumlah();
            }

        });

        for (HasilPencarian hasilPencarian: listHasil) {
            System.out.println(hasilPencarian);
        }

    }

    public static void main(String[] args) {
        try {
            new MultiThread().proses();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

}

Kode program MultiThread.java terlihat lebih rumit dibanding SingleThread.java.   Setelah setiap baris dibaca dari file, kode program tersebut akan memakai sebuah RecursiveTask untuk memisahkan setiap baris menjadi kata.  Setelah selesai, hasilnya akan diproses lagi oleh RecursiveTask lain yang menghitung jumlah kata. Karena prosesnya dilakukan secara concurrent, saya perlu mengurutkan kata berdasarkan abjad terlebih dahulu. Tujuannya adalah agar kata yang sama tidak diproses oleh thread yang berbeda (bila hal ini terjadi, hasil perhitungan akan salah!).

RecursiveTask pertama yang bertugas untuk memisahkan setiap baris menjadi kata akan saya beri nama sebagai TaskPemisah, isinya akan terlihat seperti berikut ini:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RecursiveTask;

public class TaskPemisah extends RecursiveTask<List<HasilPencarian>> {

    private List<String> listBaris;
    private int indexMulai;
    private int indexSelesai;

    public TaskPemisah(List<String> listBaris, int indexMulai, int indexSelesai) {
        this.listBaris = listBaris;
        this.indexMulai = indexMulai;
        this.indexSelesai = indexSelesai;
    }

    @Override
    protected List<HasilPencarian> compute() {
        List<HasilPencarian> listHasil = new ArrayList<>();
        if ((indexSelesai - indexMulai) < 10) {
            for (int i=indexMulai; i<=indexSelesai; i++) {
                String baris = listBaris.get(i);
                for (String kata: baris.split(" ")) {
                    if (kata.trim().length()==0) break;
                    kata = kata.trim().toLowerCase();
                    HasilPencarian hasilPencarian = new HasilPencarian(kata, 1);
                    listHasil.add(hasilPencarian);
                }
            }
        } else {
            int indexTengah = (indexSelesai + indexMulai) / 2;
            TaskPemisah task1 = new TaskPemisah(listBaris, indexMulai, indexTengah);
            TaskPemisah task2 = new TaskPemisah(listBaris, indexTengah+1, indexSelesai);
            invokeAll(task1, task2);
            try {
                listHasil.addAll(task1.get());
                listHasil.addAll(task2.get());
            } catch (InterruptedException | ExecutionException ex) {
                ex.printStackTrace();
            }
        }
        return listHasil;
    }

}

RecursiveTask yang akan menghitung jumlah kata akan saya beri nama sebagai TaskPenghitung, isinya akan terlihat seperti berikut ini:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RecursiveTask;

public class TaskPenghitung extends RecursiveTask<List<HasilPencarian>>{

    private List<HasilPencarian> listProses;
    private int indexMulai;
    private int indexSelesai;

    public TaskPenghitung(List<HasilPencarian> listProses, int indexMulai, int indexSelesai) {
        this.listProses = listProses;
        this.indexMulai = indexMulai;
        this.indexSelesai = indexSelesai;
    }

    private void proses(List<HasilPencarian> listHasil) {
        for (int i=indexMulai; i<=indexSelesai; i++) {
            HasilPencarian hasilPencarian = listProses.get(i);
            int index = listHasil.indexOf(hasilPencarian);
            if (index >= 0) {
                listHasil.get(index).tambahJumlah(hasilPencarian);
            } else {
                listHasil.add(hasilPencarian);
            }
        }
    }

    @Override
    protected List<HasilPencarian> compute() {
        List<HasilPencarian> listHasil = new ArrayList<>();
        if ((indexSelesai - indexMulai) < 1000) {
            proses(listHasil);
        } else {
            int tengah = (indexMulai + indexSelesai) / 2;
            while (true) {
                if (tengah==indexSelesai) {
                    proses(listHasil);
                    return listHasil;
                }
                String kataSekarang = listProses.get(tengah).getKata();
                String kataBerikut = listProses.get(tengah+1).getKata();
                if (!kataSekarang.equals(kataBerikut)) break;
                tengah++;
            }

            TaskPenghitung task1 = new TaskPenghitung(listProses, indexMulai, tengah);
            invokeAll(task1);

            TaskPenghitung task2 = null;
            if (tengah<indexSelesai) {
                task2 = new TaskPenghitung(listProses, tengah+1, indexSelesai);
                invokeAll(task2);
            }

            try {
                listHasil.addAll(task1.get());
                if (task2!=null) {
                    listHasil.addAll(task2.get());
                }
            } catch (InterruptedException | ExecutionException ex) {
                ex.printStackTrace();
            }
        }
        return listHasil;
    }

}

Bila dijalankan, hasilnya akan sama persis seperti pada versi SingleThread.java.  Yang jelas, kode program MultiThread.java lebih panjang, lebih rumit, dan jumlah class-nya tambah banyak.  Apakah kerumitan ini memiliki manfaat?

Saya akan mulai dengan melakukan profiling kinerja.  Hasil CPU sampling dapat dilihat pada gambar berikut ini:

CPU Sampling Pada Aplikasi Versi Multi Thread

CPU Sampling Pada Aplikasi Versi Multi Thread

Terlihat bahwa waktu yang dibutuhkan oleh MultiThread.java hanya 7,1 detik;  bandingkan dengan SingleThread.java yang membutuhkan 24,6 detik.   Peningkatan yang terjadi mencapai 71 %.  Ini adalah sebuah selisih yang cukup jauh.

Untuk membuktikan bahwa ini adalah aplikasi multithreading, saya akan melakukan profiling untuk me-monitor thread, dimana hasilnya akan terlihat seperti pada gambar berikut ini:

Tampilan Thread Pada Aplikasi Multi Thread

Tampilan Thread Pada Aplikasi Multi Thread

Pada gambar di atas, selain terdapat thread untuk main, juga ada thread ForkJoinPool-1-worker-1 dan ForkJoinPool-1-worker-1 (terlihat bahwa kode program belum maksimal karena masih sedikit yang ‘hijau’ secara bersamaan!).  Mereka diciptakan oleh class ForkJoinPool yang dipakai di MultiThread.java.   Secara default, jumlah thread yang diciptakan berdasarkan jumlah core CPU (prosesor) yang terdeteksi (pada prosesor yang memakai Hyper-Threading Technology, yang dipakai adalah jumlah virtual core).   Developer dapat menentukan nilai yang berbeda dengan melewatkan sebuah angka yang mewakili jumlah thread yang akan dibuat (nilai parallelism) di constructor ForkJoinPool.

ASM #5: Memakai Agen

Pada tulisan sebelumnya, class hasil transformasi tidak dapat dipakai secara langsung, melainkan harus memakai interface atau superclass-nya. Bagaimana jika aku tetap ingin memakai seperti biasa tanpa membuat interface atau superclass? Salah satu solusi yang mungkin adalah dengan memakai agent. Class tetap dapat dipakai seperti biasa, bahkan tanpa membuat classloader baru. Hal ini karena agent akan dikerjakan saat JVM dijalankan.

JVM akan mencari dan mengerjakan method milik agent class yang definisinya seperti berikut:

public static void premain(String agentArgs, Instrumentation inst);
atau
public static void premain(String agentArgs);

Berikut ini adalah contoh agent class yang aku pakai:

public class Agent {

  public static void premain(String agentArgs, Instrumentation inst) {
    inst.addTransformer(new ClassFileTransformer(){

      @Override
      public byte[] transform(ClassLoader loader, String className,
          Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
          byte[] classfileBuffer) throws IllegalClassFormatException {

        if (className.equals("latihan/Music")) {
          try {

            ClassReader reader = new ClassReader(classfileBuffer);
            ClassWriter writer = new ClassWriter(reader, 0);
            ToStringAdapter adapter = new ToStringAdapter(writer);

            reader.accept(adapter,0);
            return writer.toByteArray();

          } catch (Exception ex) {
            ex.printStackTrace();
          }
        }
        return classfileBuffer;
      }

    });
  }

}

Agent di atas akan melakukan transformasi melalui ToStringAdapter (turunan dari ClassAdapter) yang sudah aku buat di tulisan sebelumnya. Untuk dapat memakai agent ini, aku masih harus membuat jar dari class ini, serta menambahkan manifest yang isinya sebagai berikut:

Manifest-Version: 1.0
Main-Class: latihan.Main
Premain-Class: latihan.Agent
Boot-Class-Path: lib/asm-3.1.jar lib/asm-commons-3.1.jar lib/asm-util-3.1.jar

Entry yang dibutuhkan untuk agent adalah Premain-Class dan Boot-Class-Path. Fungsinya dapat ditebak dari namanya: Premain-Class sama seperti Main-Class, hanya saja ia menyatakan class mana yang merupakan agent. Boot-Class-Path sama seperti Class-Path, hanya saja ia menyatakan classpath yang khusus dipakai oleh agent.

Sekarang, class Music dapat dipakai layaknya class normal lainnya (tentu saja dengan pengecualian sudah punya definisi method toString() secara otomatis):

public class Main {

  public Main() {
    Music m = new Music();
    m.setMusicID("MID-123");
    m.setTitle("TITLE");
    System.out.println("Music = " + m);
  }

  public static void main(String[] args) throws Exception {
    new Main();
  }

}

Btw, untuk menjalankan program di atas, aku tidak lupa menambahkan argument -javaagent agar JVM mengerjakan agent class, seperti:

java -javaagent:latihanASM.jar -jar latihanASM.jar

ASM #3: Kode-Kode Bahasa Java

Hari ini aku akan mempelajari bagaimana byte code Java (atau bahasa assembler-nya Java) bekerja. Yang akan menjadi “bahan penelitian” adalah method berikut:

public String toString() {
  return "[MUSIC]: MusicID=[" + musicID + "]; title=[" + title + "]";
}

Setelah di-compile, method di atas akan memiliki atribut code seperti berikut:

00 03 00 01 00 00 00 25 
bb 00 27 59 12 29 b7 00 
2b 2a b4 00 14 b6 00 2d 
12 31 b6 00 2d 2a b4 00 
16 b6 00 2d 12 33 b6 00 
2d b6 00 35 b0 00 00 00 
02 00 18 00 00 00 06 00 
01 00 00 00 1f 00 19 00 
00 00 0c 00 01 00 00 00 
25 00 1a 00 1b 00 00

Wew, bagaimana menerjemahkan byte code yang tampak asing ini? Dua byte pertama, 00 03, adalah jumlah maksimum untuk operand stack. Nilai ini harus dihitung oleh compiler secara manual. Dua byte berikutnya, 00 01, adalah jumlah variabel lokal yang dipakai dalam method (termasuk parameter). Empat byte berikutnya, 00 00 00 25, menunjukkan kalau ada 0x25 (desimal 37) byte berikutnya yang berisi byte code. Dengan demikian, bagian yang benar-benar berisi byte code adalah 25 byte berikutnya, yaitu:

bb 00 27 59 12 29 b7 00 
2b 2a b4 00 14 b6 00 2d 
12 31 b6 00 2d 2a b4 00 
16 b6 00 2d 12 33 b6 00
2d b6 00 35 b0

Byte pertama, 0xbb menunjukkan bahwa instruksi tersebut adalah instruksi new. Dua byte berikutnya, 00 27, adalah referensi ke constant pool index ke-0x27 atau ke-39, dimana berupa class java/lang/StringBuilder. Ini artinya, JVM akan membuat instance baru dari class java.lang.StringBuilder dan men-push referensi instance tersebut ke operand stack.

Instruksi berikutnya adalah, 0x59, adalah instruksi dup. Instruksi ini tidak membutuhkan operand. Pada saat menemukan instruksi ini, JVM akan men-push nilai yang sama dengan nilai yang berada paling atas di operand stack saat ini.

Operand Stack (Sebelum dup):

| Instance StringBuilder | <-- TOP

Operand Stack (Setelah dup):

| Instance StringBuilder | <-- TOP
| Instance StringBuilder |

Instruksi berikutnya 0x12, adalah instruksi ldc. Instruksi ini akan men-load operand-nya (byte berikut-nya, yaitu 0x29 atau 41) yang merupakan referensi index di constant pool ke operand stack. Index ke-41 di operand stack adalah sebuah String “[MUSIC]: MusicID=[“. Dengan demikian, isi operand stack akan menjadi:

Operand Stack (Setelah ldc):
| "[Music: MusicID=["      |  <-- TOP
| Instance StringBuilder   |
| Instance StringBuilder   |

P.S: Jumlah operand stack tidak boleh melebihi maksimum yang telah ditentukan sebelumnya, yaitu 3. Jika ada perintah yang men-push sekali lagi, maka JVM akan memunculkan pesan kesalahan.

Instruksi berikutnya, 0xb7, adalah instruksi invokespecial. Instruksi ini membutuhkan dua byte operand, 00 2b (desimal 43), yang merupakan referensi ke method <init> di constant pool. Selain itu, instruksi ini akan mengambil informasi instance class mana yang akan dikerjakan method-nya melalui informasi di operand stack. Karena method yang didefinisikan di index 43 adalah constructor StringBuffer yang mengambil sebuah parameter String, maka isi operand stack menjadi:

Operand Stack (Setelah invokespecial):
| Instance StringBuilder   | <-- TOP

Berikutnya, terdapat instruksi 0x2a atau aload_0. Instruksi ini akan me-load nilai yang terdapat di local variable array yang berada di index 0 ke operand stack. Seperti yang kita tahu, nilai yang paling awal di local variable array adalah referensi ke class yang sedang aktif (this). Akibatnya, isi operand stack akan menjadi:

Operand Stack (Setelah aload_0):
| this (instance class Music)      | <-- TOP
| Instance StringBuilder           |

Instruksi berikutnya, 0xb4, adalah instruksi getfield. Instruksi ini akan men-push nilai field yang ditunjukkan oleh dua byte berikutnya, 00 14 (desimal 20), ke operand stack. Sebelumnya, ia akan men-pop terlebih dahulu dari operand stack untuk mengetahui class mana yang akan diambil nilai field-nya. Karena index ke-20 adalah referensi ke field musicID, maka isi operand stack akan menjadi:

Operand Stack (Setelah getfield):
| "nilai musicID"        | <-- TOP
| Instance StringBuilder |

Instruksi berikutnya, 0xb6, adalah instruksi invokevirtual. Instruksi ini akan mengerjakan method yang direferensikan oleh dua byte berikutnya, 00 2d (atau desimal 45), yaitu method append. Setelah pengerjaan method append, isi operand stack akan menjadi:

Operand Stack (Setelah invokevirtual):
| Instance StringBuilder | <-- TOP

Instruksi berikutnya, 0x12, kembali lagi merupakan instruksi ldc, untuk men-push String “]; title=[” ke operand stack. Setelah itu, instruksi 0xb6 (invokevirtual) akan mengerjakan method append milik StringBuffer. Selanjutnya, instruksi 0x2a (aload_0) akan me-load nilai this ke operand stack. Dan instruksi 0xb4 (getfield) akan mengambil nilai dari field title dan men-push nilai tersebut ke operand stack. Instruksi 0xb6 (invokevirtual) kembali mengerjakan method append. Instruksi 0x12 (ldc) yang berikutnya akan men-push String “]” ke operand stack. Instruksi berikutnya 0xb6 kembali mengerjakan method append. Instruksi 0xb6 berikutnya akan mengerjakan method toString milik StringBuffer. Dan instruksi terakhir, 0xb0 adalah instruksi areturn, yang akan keluar dari method serta mengembalikan nilai berupa referensi class yang akan di-pop dari operand stack.

Wew, perjalanan yang cukup panjang hanya untuk sebuah method yang sangat sederhana, bahkan hanya satu baris saja. Untungnya, aku tidak perlu selalu menerjemahkan bytecode dengan cara manual seperti ini. Di situs ObjectWeb, dimana aku mendownload ASM, aku juga dapat men-download plugin Eclipse untuk melihat isi byte code dari sebuah source code Java. Setelah meng-install plugin tersebut, aku dapat memilih Window, Show View, Byte Code. Akan muncul sebuah window Byte Code di sebelah kanan workbench yang akan berisi dissasembler dari source code Java dimana kursor editor sedang aktif. Ini adalah output untuk method yang aku pakai di tulisan ini:

  public toString()Ljava/lang/String;
   L0
    LINENUMBER 31 L0
    NEW java/lang/StringBuilder
    DUP
    LDC "[MUSIC]: MusicID=["
    INVOKESPECIAL java/lang/StringBuilder.<init>(Ljava/lang/String;)V
    ALOAD 0
    GETFIELD latihan/Music.musicID : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "]; title=["
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    GETFIELD latihan/Music.title : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "]"
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String;
    ARETURN
   L1
    LOCALVARIABLE this Llatihan/Music; L0 L1 0
    MAXSTACK = 3
    MAXLOCALS = 1

ASM #2: Membedah File Class

Hari ini aku akan mempelajari bagaimana format sebuah file class. Untuk itu, aku membuat sebuah file class dari source code sederhana berikut:

package latihan;

public class Music {

  private String musicID;
  private String title;
  private final String musicPrefix = "01";

  public Music(String musicId, String title) {
    super();
    musicID = musicId;
    this.title = title;
  }
  public String getMusicID() {
    return musicID;
  }
  public void setMusicID(String musicId) {
    musicID = musicId;
  }
  public String getTitle() {
    return title;
  }
  public void setTitle(String title) {
    this.title = title;
  }
  public static Music getMusic(String id) {
    return null;
  }

}

Setelah selesai men-compile Music.java menjadi Music.class, aku membuka file Music.class melalui hex editor untuk melihat isinya. Setiap file class Java selalu diawali dengan empat byte 0xca, 0xfe, 0xba, 0xbe. Cafe Babe, huh? Setelah itu terdapat dua byte yang berisi minor version (nilainya 0) dan dua byte yang berisi major version (nilainya 50 pada contoh class-ku).

Dua byte berikutnya, 00 35, menunjukkan bahwa di dalam constant pool terdapat 34 item. Nilai ini merupakan jumlah item di constant pool ditambah dengan 1. Setelah itu, berikutnya adalah kumpulan byte untuk constant pool. Ukuran dan isinya dapat berbeda-beda tergantung kode program. Secara umum, constant pool terdiri atas satu atau lebih item, dimana setiap item selalu diawali dengan sebuah byte yang berisi jenis item (byte ini disebut tag).

Sebagai contoh, berikut ini adalah sebagian isi constant pool untuk kode program di atas:

Item #1
Tag: Method References
Class Index: 7
Name & Type Index: 27

...

Item #7
Tag: Class
Name Index: 33

...

Item #13
Tag: UTF8
String Value: <init>

...

Item #25
Tag: UTF8
String Value: SourceFile

Item #26
Tag: UTF8
String Value: Music.java

Item #27
Tag: Name & Type
Name Index: 13
Descriptor Index: 34

...

Item #33
Tag: UTF8
String Value: java/lang/Object

Item #34
Tag: UTF8
String Value: ()V

Item pertama menunjukkan informasi method, untuk class yang ada di item ke-7 (yaitu java/lang/Object). Informasi mengenai method yang ditunjukkan oleh item pertama dapat dilihat lebih lanjut di item ke-27, yang selanjutnya memberikan informasi mengenai method bernama “<init>” (item ke-13) yang descriptor-nya adalah “()V” (item ke-34). Btw, aku tidak membuat method dengan nama “<init>” di source code, darimana munculnya method “<init>”? Ini adalah nama yang khusus diberikan untuk constructor.

Setelah isi constant pool, terdapat dua byte yang berisi access flag. Nilai flag ini menunjukkan apakah class ini termasuk class final, abstract, atau merupakan interface. Nilai untuk class percobaan hari ini adalah 0x0021 yang merupakan kombinasi dari flag 0x0001 (class public) dan flag 0x0020 (selalu harus di-set untuk compiler baru).

Berikutnya terdapat dua byte yang merujuk pada item index di constant pool yang berisi referensi class ini (class yang dirujuk oleh keyword this). Lalu, berikutnya terdapat dua byte yang merujuk pada item index di constant pool yang berisi referensi super-class (parent-class). Dalam contoh class percobaan, super class-nya adalah java/lang/Object (item ke-7 di constant pool). Nilai dua byte ini dapat berupa 0x0000, jika class ini adalah java/lang/Object (satu-satunya class di Java yang tidak punya super class).

Setelah itu terdapat dua byte yang berisi jumlah inteface yang di-implement oleh class ini, diikuti dengan rangkaian index di constant pool untuk menjelaskan interface tersebut. Karena class percobaanku tidak men-implementasi-kan interface, nilai interface count adalah 0.

Berikutnya adalah dua byte yang berisi jumlah field/variabel yang didefinisikan dalam class ini (nilainya adalah 3, karena aku hanya mendefinisikan 3 variabel di source code). Berikutnya adalah struktur yang menjelaskan informasi field tersebut. Ukurannya bisa berbeda tergantung pada jumlah field/variabel yang didefinisikan di dalam class ini. Berikut ini adalah isi informasi field pada class percobaan hari ini:

Field #0
Access Flag:  PRIVATE
Name Index: 8
Descriptor Index: 9
Attribute Count: 0

Field #1
Access Flag:  PRIVATE
Name Index: 10
Descriptor Index: 9
Attribute Count: 0

Field #2
Access Flag:  PRIVATE  FINAL
Name Index: 11
Descriptor Index: 9
Attribute Count: 1
  Attribute Name Index: 12
  Attribute Length: 2 [00 02 ]

Setelah informasi field, terdapat informasi mengenai method. Dua byte pertama, seperti biasa, menunjukkan jumlah method yang didefinisikan dalam class ini (nilainya adalah 6, karena aku mendefinisikan 6 method di source code). Berikutnya, terdapat informasi mengenai method. Pada bagian ini terdapat informasi byte code Java untuk masing-masing method yang terletak di atribut dengan nama “Code”. Aku akan mempelajari lebih lanjut tentang atribut ini di kemudian hari. Ini adalah contoh informasi method pada class percobaan:

Method #0
Access Flag:  PUBLIC
Name Index: 13
Descriptor Index: 14
Attribute Count: 1
Atribute Name Index: 15
Atribute Length: 61
00 02 00 03 00 00 00 15
2a b7 00 01 2a 12 02 b5
00 03 2a 2b b5 00 04 2a
2c b5 00 05 b1 00 00 00
01 00 10 00 00 00 16 00
05 00 00 00 0a 00 04 00
07 00 0a 00 0b 00 0f 00
0c 00 14 00 0d 

Method #1
Access Flag:  PUBLIC
Name Index: 17
Descriptor Index: 18
Attribute Count: 1
Atribute Name Index: 15
Atribute Length: 29
00 01 00 01 00 00 00 05
2a b4 00 04 b0 00 00 00
01 00 10 00 00 00 06 00
01 00 00 00 0f 

Method #2
Access Flag:  PUBLIC
Name Index: 19
Descriptor Index: 20
Attribute Count: 1
Atribute Name Index: 15
Atribute Length: 34
00 02 00 02 00 00 00 06
2a 2b b5 00 04 b1 00 00
00 01 00 10 00 00 00 0a
00 02 00 00 00 12 00 05
00 13 

Method #3
Access Flag:  PUBLIC
Name Index: 21
Descriptor Index: 18
Attribute Count: 1
Atribute Name Index: 15
Atribute Length: 29
00 01 00 01 00 00 00 05
2a b4 00 05 b0 00 00 00
01 00 10 00 00 00 06 00
01 00 00 00 15 

Method #4
Access Flag:  PUBLIC
Name Index: 22
Descriptor Index: 20
Attribute Count: 1
Atribute Name Index: 15
Atribute Length: 34
00 02 00 02 00 00 00 06
2a 2b b5 00 05 b1 00 00
00 01 00 10 00 00 00 0a
00 02 00 00 00 18 00 05
00 19 

Method #5
Access Flag:  PUBLIC  STATIC
Name Index: 23
Descriptor Index: 24
Attribute Count: 1
Atribute Name Index: 15
Atribute Length: 26
00 01 00 01 00 00 00 02
01 b0 00 00 00 01 00 10
00 00 00 06 00 01 00 00
00 1b

Dan bagian yang paling terakhir dari sebuah file class Java adalah atribut untuk class tersebut. Dua byte pertama berisi informasi jumlah atribut, di-ikuti dengan definisi atribut. Ini adalah contoh informasi atribut untuk class percobaanku:

Attribute #0
Attribute Name Index: 25
Attribute Length: 2
00 1a

Atribut tersebut adalah atribut “Source File” yang nilainya adalah referensi ke item 0x1a (atau desimal 26) di constant pool, yang nilainya tidak lain adalah “Music.java”.

ASM #1: JVM, Mesin Yang Tidak Nyata

Untuk melakukan bytecode instrumentation melalui library seperti ASM, aku setidaknya harus mengerti bagaimana cara kerja JVM.  Setiap program Java, atau tepatnya class Java, akan dijalankan oleh Java Virtual Machine (JVM).  Mirip seperti mesin asli, JVM juga memiliki struktur-struktur seperti register dan stack.

Masing-masing thread yang dijalankan oleh JVM memiliki sebuah register Program Counter (pc).  Register pc berisi lokasi alamat instruksi yang sedang dikerjakan.  Untuk method native,  nilai register pc tidak didefinisikan.

Setiap thread di JVM juga memiliki Java Virtual Machine Stack yang kegunaannya mirip seperti di program biasa seperti C, seperti menampung nilai variabel lokal dan hasil perhitungan sementara.

Seluruh thread di JVM memiliki memory yang di-share yang disebut heap. Heap adalah lokasi memori yang berisi informasi instance dari sebuah class dan array.  Garbage collector akan bekerja secara otomatis untuk menentukan instance yang tidak dibutuhkan lagi dan membebaskan lokasi memori di heap sehingga memori dapat dipakai ulang.

Setiap kali sebuah method dipanggil, JVM akan membuat sebuah frame di Java Virtual Machine stack untuk thread bersangkutan.  Setelah method selesai dikerjakan, JVM akan memusnahkan frame tersebut.   Di dalam frame terdapat informasi local variables.  Sebuah “slot” local variable dapat mengandung nilai boolean, byte, char, short, int, float, reference, atau returnAddress.  Nilai long atau double memerlukan dua “slot”  local variable.

Setiap “slot” local variable memiliki index berurut mulai dari 0, 1, 2, dst.   Pada saat method mulai dijalankan, local variable 0 akan berisi referensi ke object yang mengandung method tersebut (nilai this dalam program Java).  Local variable 1, 2, dst berisi nilai parameter secara berurut.

Frame juga mengandung apa yang disebut operand stack yang banyak dipakai oleh instruksi JVM untuk menulis dan membaca nilai.  Pada saat frame pertama kali dibuat, operand stack tidak memiliki isi.

Instruksi JVM terdiri atas sebuah byte yang berisi opcode, diikuti oleh operands (jika ada).  Kebanyakan instruksi JVM bekerja pada tipe data tertentu.  Sebagai contoh, instruksi yang diawali dengan huruf i bekerja pada data int, huruf l bekerja pada data long, huruf s bekerja pada data short, huruf b bekerja pada data byte, huruf c bekerja pada data char, huruf f bekerja pada data float, huruf d bekerja pada data double, dan huruf a bekerja pada data reference.  O ya, di bahasa pemograman Java ada tipe data boolean, tapi  JVM tidak mengenal istilah boolean.   JVM akan menganggap boolean sebagai integer.

Instruksi load dan store dipakai untuk mengambil nilai dan menulis nilai ke dalam local variable. Sebagai contoh, iload 1 akan menulis local variable 1 yang bertipe int ke dalam operand stack.  Sebaliknya, istore 1 akan menulis nilai int yang ada di operand stack ke local variable 1.

Contoh instruksi aritmatika yang tersedia seperti iadd (penjumlahan), isub (pengurangan), imul (perkalian), idiv (pembagian),  irem (modulus), ineg (negation), ishl (shift left), ishr (shift right), ior (bitwise OR), iand (bitwise AND), ixor (bitwise XOR), iinc (local variable increment), lcmp (perbandingan).  Sebagai contoh, instruksi iadd akan menjumlahkan dua nilai int yang ada di operand stack (men-pop dua nilai terakhir), kemudian menyimpan hasil penjumlahan ke operand stack (men-push hasil penjumlahan).

JVM juga menyediakan beberapa instruksi untuk konversi, seperti i2l (int to long), i2f (int to float), i2d (int to double), l2f (long to float), l2d (long to double) dan f2d (float to double).

Untuk membuat instance class baru, terdapat instruksi new.  Untuk membuat array baru, tersedia instruksi newarray, anewarray, dan multianewarray.  Untuk meng-akses field dari sebuah object, instruksi berikut dapat dipergunakan: getfield, putfield, getstatic, putstatic.  Beberapa instruksi lain yang berkaitan dengan class: arraylength, instanceof, dan checkcast.

Instruksi berikut dipakai untuk memanipulasi operand stack: pop, pop2, dup, dup2, dup_xl, dup2_xl, dup_x2, dup2_x2, dan swap.

Contoh instruksi yang dipakai untuk control transfer (percabangan), misalnya: ifeq (jika sama dengan), iflt (jika lebih kecil), tableswitch, goto, dan ret.

Untuk memanggil method dari sebuah class, instruksi berikut dapat dipergunakan: invokevirtual, invokeinterface, invokespecial, dan invokestatic.  Untuk keluar dari method, instruksi berikut dapat dipergunakan sesuai dengan tipe kembalian dari method: return, ireturn, lreturn, freturn, dreturn, dan areturn.  Untuk membuat exception, instruksi athrow dapat dipergunakan.

Untuk mendukung sinkronisasi (keyword synchronize di bahasa pemograman Java), JVM menggunakan monitor, dan menyediakan instruksi berikut: monitorenter dan monitorexit.