How to Create an Excel File in Java (Step-by-Step)

Creating an Excel file in Java lets applications deliver sales summaries, inventory lists, and other business data in a format people can review and edit. In this guide, we’ll use Axyra Sheets for Java to build an XLSX sales report from scratch, with headers, numeric data, formulas, number formatting, and a frozen header row. The steps come together in a complete program that saves a workbook you can open in Excel.

Already know the object model? The Quick Start is the shorter version. This guide is the longer one, and it produces a file you would be willing to send to someone.

What you need

  • Java 17 or later. The API uses sealed interfaces and records.
  • The dependency. Native libraries for the supported platforms are bundled in the jar, so there is nothing else to install.
<dependency>
    <groupId>io.keikai</groupId>
    <artifactId>axyra-sheets</artifactId>
    <version>1.0.0-Beta.FL.20261013-Eval</version>
</dependency>
repositories {
    mavenCentral()
    maven { url "https://mavensync.zkoss.org/eval" }
}

dependencies {
    implementation "io.keikai:axyra-sheets:1.0.0-Beta.FL.20261013-Eval"
}

The artifact is served from ZK’s repository rather than Maven Central, so a Maven build also needs the <repositories> entry shown in Installation.

Step 1 — Create the workbook

Workbook.create() gives you an empty workbook. Add the first sheet explicitly and keep the returned view:

import io.keikai.axyra.sheets.Workbook;
import io.keikai.axyra.sheets.Sheet;

try (Workbook wb = Workbook.create()) {
    Sheet sheet = wb.createSheet("Q3 Sales");
    // ...
}

Workbook implements AutoCloseable and owns native memory that the garbage collector cannot reclaim, so the try-with-resources block is not optional. See Workbook and Sheet for the full lifecycle.

Rows and columns are 0-based; formulas are not. cell(0, 0) is A1, so the cell at row index 1 is row 2 in a formula. This trips up everyone once.

Step 2 — Write the header and the data

Set a whole block in one call rather than looping cell by cell. Each Range call crosses the JNI boundary once for the entire block; a per-cell loop crosses it once per cell.

import io.keikai.axyra.sheets.CellValue;

sheet.range("A1:D1").setValues(new CellValue[][] {
    { CellValue.text("Region"), CellValue.text("Q1"),
      CellValue.text("Q2"),     CellValue.text("Total") }
});

sheet.range("A2:C4").setValues(new CellValue[][] {
    { CellValue.text("APAC"),     CellValue.number(1200), CellValue.number(1450) },
    { CellValue.text("EMEA"),     CellValue.number(980),  CellValue.number(1100) },
    { CellValue.text("Americas"), CellValue.number(1610), CellValue.number(1720) },
});

setValues takes a row-major CellValue[][] whose shape must match the range. When every value is numeric, setNumbers(double[]) is faster still — it is a flat array with no boxing and no CellValue allocation. That is the path to use when the data comes out of a database; see Cells and Ranges.

Step 3 — Add formulas

Write the per-row total into column D, and a grand total underneath the data.

sheet.range("D2:D4").setFormula("B2+C2");          // D3 = B3+C3, D4 = B4+C4

sheet.cell(4, 0).setValue(CellValue.text("Total"));
sheet.range("B5:D5").setFormula("SUM(B2:B4)");     // C5 = SUM(C2:C4), D5 = SUM(D2:D4)

Range.setFormula on a multi-cell range fills it the way Excel’s Ctrl+Enter does: the formula text is written for the top-left cell, and relative references shift for every other cell, while $-anchored references stay fixed. Range.copyTo and Range.autoFill shift references the same way when you copy an existing formula.

Do not fill a range with a formula that spills, such as SEQUENCE(5): every cell becomes its own dynamic-array anchor and shows #SPILL!. Put a spilling formula in a single cell.

Formulas are calculated as you write them. Each write recalculates the cells that depend on it, so D5 already holds 8060 here and the saved file carries that result. Saving itself does not recalculate: call wb.recalculate() once before saving only when results can be stale — in CalcMode.MANUAL, or for untouched formulas in a workbook you opened.

Step 4 — Style it

Styles are immutable values built once and applied to a range.

import io.keikai.axyra.sheets.style.CellStyle;
import io.keikai.axyra.sheets.style.Color;
import io.keikai.axyra.sheets.style.BorderStyle;
import io.keikai.axyra.sheets.style.HAlign;

CellStyle header = CellStyle.builder()
        .bold()
        .fontColor(Color.parse("#FFFFFF"))
        .fill(Color.parse("#2F5597"))
        .horizontalAlign(HAlign.CENTER)
        .allBorders(BorderStyle.THIN, Color.parse("#1F3864"))
        .build();

CellStyle money = CellStyle.builder()
        .numberFormat("#,##0.00")
        .build();

CellStyle totalRow = CellStyle.builder()
        .bold()
        .numberFormat("#,##0.00")
        .build();

sheet.range("A1:D1").setStyle(header);
sheet.range("B2:D4").setStyle(money);
sheet.range("A5:D5").setStyle(totalRow);

numberFormat takes an Excel format code, so anything valid in Excel’s Format Cells dialog works here — "#,##0.00", "0.0%", "yyyy-mm-dd", and conditional sections such as "#,##0.00;[Red]-#,##0.00". See Styles and Formats.

If the same style repeats across sheets, register it once by name and apply it by name:

wb.registerNamedStyle("Currency", money);
sheet.range("B2:D4").applyNamedStyle("Currency");

Step 5 — Column widths and a frozen header

sheet.setColumnWidth(0, 14.0);   // Excel character units
for (int col = 1; col <= 3; col++) {
    sheet.autoFitColumn(col);
}
sheet.freezePanes(1, 0);         // keep row 1 visible while scrolling

Column width is in Excel’s character units (the width of the 0 glyph in the default font), not points — row height is the one measured in points. autoFitColumn sizes the column to the formatted text of its cells, so call it after you apply number formats, as here.

Step 6 — Save

The format comes from the file extension.

import java.nio.file.Path;

wb.save(Path.of("q3-sales.xlsx"));

To send the file straight to an HTTP response or into object storage instead, skip the disk entirely:

byte[] bytes = wb.saveBytes("xlsx");
wb.save(response.getOutputStream(), "xlsx");

.xls, .xlsb, .ods, .csv, and .json all work the same way — see Supported Formats for what each one can carry.

The complete program

import io.keikai.axyra.sheets.CellValue;
import io.keikai.axyra.sheets.Sheet;
import io.keikai.axyra.sheets.Workbook;
import io.keikai.axyra.sheets.style.BorderStyle;
import io.keikai.axyra.sheets.style.CellStyle;
import io.keikai.axyra.sheets.style.Color;
import io.keikai.axyra.sheets.style.HAlign;

import java.nio.file.Path;

public class CreateExcelFile {

    public static void main(String[] args) {
        try (Workbook wb = Workbook.create()) {
            Sheet sheet = wb.createSheet("Q3 Sales");

            // Header and data, one call each.
            sheet.range("A1:D1").setValues(new CellValue[][] {
                { CellValue.text("Region"), CellValue.text("Q1"),
                  CellValue.text("Q2"),     CellValue.text("Total") }
            });
            sheet.range("A2:C4").setValues(new CellValue[][] {
                { CellValue.text("APAC"),     CellValue.number(1200), CellValue.number(1450) },
                { CellValue.text("EMEA"),     CellValue.number(980),  CellValue.number(1100) },
                { CellValue.text("Americas"), CellValue.number(1610), CellValue.number(1720) },
            });

            // Per-row totals, then a grand-total row. References shift per cell.
            sheet.range("D2:D4").setFormula("B2+C2");
            sheet.cell(4, 0).setValue(CellValue.text("Total"));
            sheet.range("B5:D5").setFormula("SUM(B2:B4)");

            // Styling.
            CellStyle header = CellStyle.builder()
                    .bold()
                    .fontColor(Color.parse("#FFFFFF"))
                    .fill(Color.parse("#2F5597"))
                    .horizontalAlign(HAlign.CENTER)
                    .allBorders(BorderStyle.THIN, Color.parse("#1F3864"))
                    .build();
            CellStyle money = CellStyle.builder().numberFormat("#,##0.00").build();
            CellStyle totalRow = CellStyle.builder()
                    .bold()
                    .numberFormat("#,##0.00")
                    .build();

            sheet.range("A1:D1").setStyle(header);
            sheet.range("B2:D4").setStyle(money);
            sheet.range("A5:D5").setStyle(totalRow);

            // Layout.
            sheet.setColumnWidth(0, 14.0);
            for (int col = 1; col <= 3; col++) {
                sheet.autoFitColumn(col);
            }
            sheet.freezePanes(1, 0);

            wb.save(Path.of("q3-sales.xlsx"));
            System.out.println("wrote q3-sales.xlsx");
        }
    }
}

Open q3-sales.xlsx and you get a header row in white on blue, three data rows with thousands separators and two decimals, a bold total row, and a header that stays put when you scroll.

Common mistakes

Symptom Cause
Computed cells are stale in the saved file The workbook is in CalcMode.MANUAL, or formulas in an opened file were not touched. Saving does not recalculate; call recalculate() once before saving.
The formula refers to the wrong row Indices are 0-based, A1 references are 1-based. Row index 1 is row 2.
A filled range shows #SPILL! A spilling (dynamic-array) formula was written into a multi-cell range. Put it in a single cell.
Native memory grows over time in a service A Workbook escaped its try-with-resources. Close every one.
IllegalStateException on a Sheet or Range The workbook was already closed. These are views, not copies.
UnsatisfiedLinkError in a container but not locally The image was built for a different architecture, or a shading step dropped native/**. See Native Library Loading.

Next steps

Beyond Your First Excel File

Once you can create and format a workbook, you can connect it to live application data and build reports with multiple sheets, charts, and pivot tables. Axyra Sheets can also render the workbook as PDF or images, giving you both an editable Excel file and a version for sharing or previewing. The same workbook creation steps can become part of a download endpoint or a scheduled reporting job.

Get code at GitHub