Search code examples
javajar

How can I load a file from inside a jar in a static way?


I have a "default" image file that's meant to be used if the "proper" image file doesn't exist. I'm using Maven to create the jar and have placed the file under resources/images, along with the other images I already have.

The file is defined in a class that has a few defaults:

public class Defaults {
    public static final File BLANK_IMAGE = new File(relative_path_to_image);
    //Other defaults
}

This works within the IDE, but not if I try to create a jar - if I do and run it from the Command Prompt, it spits out the message I wrote for when a specific file can't be found.

I've looked at several potential solutions, but they all rely on non-static methods, which can't be used here since the File is static (also, it makes no sense to instantiate something when all its instances would be identical - that's just a waste of time and resources).

Am I just stuck converting the File to a String and processing it inside every Object that needs it with getResourceAsStream()?

The file is placed on a few JLabel instances (ones that are meant to have images that haven't been created yet) by first converting it into a BufferedImage. I mention this in case there are better ways of achieving the same result that bypass the problem I'm having.


Solution

  • new File means a file. An entry in a jar file isn't one. If you mention File anywhere in the code, the game is lost immediately.

    (also, it makes no sense to instantiate something when all its instances would be identical - that's just a waste of time and resources).

    This mindset is highly detrimental. It leads to writing bad code. Bad in all senses:

    • Significantly harder to test.
    • Slower to execute. (Lots of fast garbage is actually faster than a few cached objects. Read up on CPU cache behaviour and generational garbage collecting to figure out that 'long lived objects used in many places' is a lot worse than you might think and 'objects that are created and are nearly immediately garbage afterwards' actually costs pretty much nothing whatsoever - the exact opposite of the mindset you appear to still be labouring under).
    • Not idiomatic (java is no longer written that way; it makes interacting with other java programmers in terms of code more difficult than it needs to be).
    • Not flexible (changing code written with your mindset when requirements change and feature requests pop up, is harder than it needs to be).

    Am I just stuck converting the File to a String and processing it inside every Object that needs it with getResourceAsStream()?

    I think you severely misunderstand what File is, then. File is just a light wrapper; it has one field, of type String, which represents the path. This:

    private static final File MY_IMG_FILE = new File("/foo/bar");
    
    public JLabel codeThatIsInvokedALot() {
      return new JLabel(new ImageIcon(MY_IMG_FILE));
    }
    

    Is exactly as inefficient as:

    public JLabel codeThatIsInvokedALot() {
      return new JLabel(new ImageIcon(MyClass.class.getResource("/foo/bar"));
    }
    

    In the sense that you pay 1 point for the allocation of some objects, and 100,000,000,000 points (that's not an exaggeration, that is a roughly accurate sense of scale) for opening the jar file (or file on disk) and reading the actual bytes representing the image from it. If you're very very lucky your OS does some disk caching and will turn that into a 1 vs. 1 million gap instead.

    If you want 'efficiency', you want to load the entire image once. "Create the file object only once" is completely meaningless for performance reasons. A File object does not cache the contents of that file - just its location, and talking about 'caching' of this doesn't bear talking about (you're caching... about 80 bits worth. The lookup costs 500x more than creating it whole cloth, again not an exagerration).

    So how is it done? Not difficult at all:

    public class MyImages {
      private final ImageIcon BLANK_IMAGE;
    
      static {
        BLANK_IMAGE = resourceToImage("/images/blankImage.png");
      }
    
      public static ImageIcon getBlankImage() { return BLANK_IMAGE; }
    
      private static ImageIcon resourceToImage(String rsc) {
        return new ImageIcon(MyImages.class.getResource(rsc));
      }
    }
    

    This will:

    • The first time the JVM executes any code that so much as mentions MyImages, it'll load the MyImages class. Presumably, this will occur not quite 'on boot' but very soon afterwards naturally.
    • Any further times any code mentions MyImages, it's already loaded; classes are only loaded at most once.
    • Loading MyImages is a heavy duty job - it'll run the static block first (and any other threads that also touch MyImages will be blocked until the first thread is done loading it), and this will actually open up the jar file and read the png file, and decompress it into a raw image map.
    • MyImages, the class, will have a relatively sizable memory footprint in the heap due to having to host all those unpacked images.
    • return new JLabel(MyImages.getBlankIcon()) will (unless this is the very first time any code in this JVM execution touches MyImages) execute very quickly and doesn't touch the disk whatsoever.
    • I'm pretty sure creating 15 jlabels all with the same icon do not cost significant amounts of memory - they are all using the same underlying unpackaged pixeldata in the one instance of ImageIcon.
    • Uses the right way to do this: SomeContextClass.class.getResource. This is strictly superior to going via the classloader, or using getClass() instead of SomeContextClass.class: These commonly shown alternatives fail in exotic circumstances (bootloaders, agents, etc - these get a null classloader), fail in less exotic circumstances (modular systems where you subclass breaks getClass() style), and MyClass.class.getResource is simple, succint, idiomatic, and has fewer failure modes than all alternatives.