Java References Confuse Me

Being able to program in multiple languages is a useful skill to have, but you have to remember that things don’t always work the way you would expect. I’ve recently come to realise that my Java code has suffered due to a fundamental misunderstanding in how it works. And this has taught me a very important lesson about programmin.

An Unusual Bug


The issue I was looking at this week involved a strange bug that only happens when playing Bok’s Banging Butterflies with version 1.21.4 of Minecraft. Basically, Bottled Light Butterflies would drop Bottled Peacemaker Butterflies when broken. After investigating, I found that all Bottled Butterflies and Caterpillars would drop only the Peacemaker versions.

This was extremely odd behaviour and it left me stumped.

I found a clue after updating the datagen to use NeoForge/Minecraft’s code rather than my own. It only generated loot tables for Bottled Peacemaker Butterflies and no other species. Eventually I traced it back to a problem with block registration. Here is the code I used to register a Bottled Butterfly:

    private static DeferredHolder<Block, Block> registerBottledButterfly(int butterflyIndex) {
        String registryId = getBottledButterflyRegistryId(butterflyIndex);
        BlockBehaviour.Properties properties = createPropertiesWithKey(registryId, BottledButterflyBlock.BASE_PROPERTIES);

        // Light Butterflies glow when they are in a bottle.
        if (Arrays.asList(ButterflyInfo.TRAITS[butterflyIndex]).contains(ButterflyData.Trait.GLOW)) {
            return BLOCKS.register(registryId, () -> new BottledButterflyBlock(properties.lightLevel((blockState) -> 15)));
        }

        return BLOCKS.register(registryId, () -> new BottledButterflyBlock(properties));
    }

And this is how createPropertiesWithKey() would create some new properties for the block to use:

    private static BlockBehaviour.Properties createPropertiesWithKey(String registryId,
                                                                     BlockBehaviour.Properties baseProperties) {
        ResourceLocation blockId = ResourceLocation.fromNamespaceAndPath(ButterfliesMod.MOD_ID, registryId);
        ResourceKey<Block> resourceKey = ResourceKey.create(Registries.BLOCK, blockId);
        return baseProperties.setId(resourceKey);
    }

If you are an experienced Java programmer you may already see the problem. But as a C++ developer I’m used to seeing things a certain way, and it really wasn’t obvious to me what the problem was.

For those who can’t see it, BottledButterflyBlock.BASE_PROPERTIES is passed by reference. In other words, the same object is used and referenced by every Bottled Butterfly entry in the registry. This means that they all use the same property object that gets modified each time this method is called. Since the last one to be registered is the Peacemaker Butterfly, they all use the properties for that species over their own.

Once I had figured this out, the fix was simple. Instead of using a global BASE_PROPERTIES object, we simply create a new Properties object every time we register a block:

    private static DeferredHolder<Block, Block> registerBottledButterfly(int butterflyIndex) {
        String registryId = getBottledButterflyRegistryId(butterflyIndex);

        // Create a new instance of these properties every time.
        BlockBehaviour.Properties properties = createPropertiesWithKey(registryId, BlockBehaviour.Properties.ofFullCopy(Blocks.GLASS)
                .isRedstoneConductor(BlockRegistry::alwaysFalse)
                .isSuffocating(BlockRegistry::alwaysFalse)
                .isValidSpawn(BlockRegistry::alwaysFalse)
                .isViewBlocking(BlockRegistry::alwaysFalse)
                .noOcclusion()
                .sound(SoundType.GLASS)
                .strength(0.3F));

        // Light Butterflies glow when they are in a bottle.
        if (Arrays.asList(ButterflyInfo.TRAITS[butterflyIndex]).contains(ButterflyData.Trait.GLOW)) {
            return BLOCKS.register(registryId, () -> new BottledButterflyBlock(properties.lightLevel((blockState) -> 15)));
        }

        return BLOCKS.register(registryId, () -> new BottledButterflyBlock(properties));
    }

I could apply the same fix for Bottled Caterpillars, and incidentally also for Butterfly Origami which I discovered had also been affected by the same bug.

The Lesson


My background is as a C++ programmer, so the way I approach programming is influenced by that even when writing in other languages. The reason I missed this bug is because in C++ everything is passed by copy unless it is explicitly stated to be a reference or pointer.1

When Java first came out, one of it’s big sells was that it “didn’t need complicated pointers.” This can be a good thing in a memory managed language, as it simplifies things and allows developers to focus on other areas of their design. Unfortunately this sell led me to believe that Java doesn’t use references.

Of course, the actual truth is that in Java, everything is a reference.2 Because I’ve gotten so used to pass-by-copy being the default, I often forget that most objects will be passed into a method as a reference. This isn’t the first time I’ve been stung by a bug like this, and I’m sure it won’t be the last.

This week has been an important reminder to me that just because I am very good at programming one language, it doesn’t make me good with all languages. Each language has its own nuance and way of doing things under the hood, and its important to try and learn these idiosyncrasies if you want to become an expert.

For me it’s part of the fun. I love that there is still so much to learn out there, and that every day I focus on developing something new, it’s going to teach me something. I’ve been programming for 35 years now, and even at this point I’m still learning every single day.

To me, that is both the pain and the joy of programming.

  1. In C++ there are references, pointers, referenced to pointers, and many other ways of passing arguments, but they go beyond the scope of this article. For now all that’s important is that passing by reference isn’t the default. ↩︎
  2. Again, another oversimplification, but still true enough for the point I’m making here. ↩︎

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.