JTableHeader will not be displayed unless you put it in a scroll pane. The reason is that the table manually checks if it is being put into a JScrollPane, and then adds the header row in the row header space of the scroll pane.
This is a picture of a table without the enclosing JScrollPane:
Same table with the enclosing JScrollPane:
Here is a snippet from JTable that shows this ugly operation:
public void addNotify() { super.addNotify(); configureEnclosingScrollPane(); } protected void configureEnclosingScrollPane() { Container p = getParent(); if (p instanceof JViewport) { Container gp = p.getParent(); if (gp instanceof JScrollPane) { JScrollPane scrollPane = (JScrollPane)gp; // Make certain we are the viewPort's view and not, for // example, the rowHeaderView of the scrollPane - // an implementor of fixed columns might do this. JViewport viewport = scrollPane.getViewport(); if (viewport == null || viewport.getView() != this) { return; } scrollPane.setColumnHeaderView(getTableHeader()); // scrollPane.getViewport().setBackingStoreEnabled(true); Border border = scrollPane.getBorder(); if (border == null || border instanceof UIResource) { scrollPane.setBorder(UIManager.getBorder("Table.scrollPaneBorder")); } } } }
The reason for this is probabily the incompletenes of the Scrollable interface - that does not define a header component. So, there are two solutions: put JTable into a JScrollPane, or if you do not want to use JScrollPane, install the header by hand above table body - however, remember to align it properly. In my experience, it is much easier to put a JTable in the JScrollPane - you can hide the scroll bars if they are not needed.
See also Avoid Double Scroll, Always Put JTable In JScrollPane.