79. Class Methods and Variables

In the preceding chapters we looked at instance properties/fields (variables) and instance methods (including constructors) - the components of a class that pertain to individual instantiations of the class. This ensures that each instance works against its own state and doesn’t interfere with other instances. There are many times, however, when we want a class to provide a method that isn’t bound to an instance or when we want a field/property that is used across instances. This is where class methods and class variables are utilised.

So, when to use class methods and variables? There’s a number of answers to this but the following three are the main ones:

When a single property is best shared across all instances
This is often the case with configuration properties
When a method isn’t really specific to an instance
The method sits nicely with the class’s concepts but doesn’t need to relate directly to each instance
When the class is really just a library of methods and constants
Sometimes you just need a set of utility methods

It’s important to note a few things about class methods:

  • They can’t access this as there’s no underlying instance
  • They can’t use instance (member) variables

Declaring a field/property or method as static is easy: you just prepend the static modifier. Lets look at an example:

A brief example of class methods and variables
import groovy.json.JsonSlurper

class Configuration {
    static String databaseName = ''
    static String databasePassword = ''
    static String logFile = ''

    static loadConfig() {
        //This is the config file:
        File file = new File('config.json')

        //We use JsonSlurper to read a JSON file:
        JsonSlurper slurper = new JsonSlurper()

        //Now parse the config file
        def config = slurper.parse(file)

        //We can access the config file elements using dot-point notation:
        databaseName = config.database
        databasePassword = config.password
        logFile = config.log

    }
}

Configuration.loadConfig()

println """\
    System database: ${Configuration.databaseName}
    System database password: ${Configuration.databasePassword}
    Log file: ${Configuration.logFile}
"""

The code above provides a common use of static variables as it reads in configuration from a source and makes that configuration available to the rest of the system. In this case I have chosen to use JSON notation to store the configuration and the file config.json appears thus:

config.json
{
  "database": "CorporateData",
  "password": "password",
  "log": "/tmp/log.txt"
}

In the Configuration class you can see three class variables: databaseName, databasePassword and logFile. These are accessed via the class, and not an instance, by using the <class name>.<variable name> form: Configuration.databaseName. I’ve also defined the class method loadConfig and this is accessed through Configuration.loadConfig. You’ll see that I don’t have to instantiate Configuration by calling new nor do I assign Configuration to a variable - I just access Configurations’s class members when I need them.

I’ve used the String type for the static variables in Configuration but, should I want to use dynamic types, I just use static <varName> and don’t need def:

static databaseName = ''
static databasePassword = ''
static logFile = ''

Whilst the Configuration class only defines class methods and variables, classes can have a mix of these elements. The next example is more complex:

A mix of static and instance elements
@Grab('org.javamoney:moneta:1.0')

import groovy.transform.ToString
import org.javamoney.moneta.Money

@ToString(includeNames = true)
class SilverCreditCard {
    String cardNumber
    String cardHolderName
    Money balance = Money.of(0, currency)

    static String currency = 'AUD'
    static Money creditLimit = Money.of(5_000, currency)

    static SilverCreditCard applyForAccount(String applicantName, Money totalAss\
ets, Money totalDebts) {
        if (totalAssets.subtract(totalDebts).subtract(creditLimit).isPositive())\
 {
            // TODO: Create a new record in the database etc
            return new SilverCreditCard(cardNumber: '0000 1111 2222 3333', cardH\
olderName: applicantName)
        } else {
            // TODO: Throw an exception - don't just return null
            return null
        }
    }

    static SilverCreditCard loadAccount(String cardNumber) {
        // TODO: Lookup the number in our database
        new SilverCreditCard(cardNumber: cardNumber, cardHolderName: 'Fred Nurk'\
, balance: Money.of(100, currency))
    }

    Money deposit(Money amt) {
        //TODO: Implement
    }

    Money withdrawl(Money amt) {
        //TODO: Implement
    }
}

SilverCreditCard yourCard = SilverCreditCard.loadAccount('1234 5678 9876 5432')
println yourCard

SilverCreditCard myCard = SilverCreditCard.applyForAccount('Jim Smith',
        Money.of(20_000, SilverCreditCard.currency),
        Money.of(10_000, SilverCreditCard.currency))

println myCard

Running this code will cause an error to be displayed but don’t panic - it’s just saying that I haven’t provided a configuration for the Java money classes. It’s all cool and fine for this example.

In the code above I have described a credit card account - the SilverCreditCard. This class uses class variables to describe the policy for the credit card product (bank speak) and class methods for accessing individual accounts (after all, you don’t want people to just new CreditCard do you?):

  • Class variables:
    • currency is the currency used by all Silver Credit Cards
    • creditLimit is the maximum limit allowed for this type of account
  • Class methods:
    • applyForAccount is for new customers requesting an account - it would return an instance of SilverCreditCard being the newly created account
    • loadAccount is for existing customers wanting to access their account - it would return an instance of SilverCreditCard with information loaded from a database

The instance elements represent a single customer’s SilverCreditCard account and aid in tracking their account:

  • Instance properties:
    • cardNumber is the unique number for the account
    • cardHolderName is the name of the card holder
    • balance is how much they’ve spent on their credit card
  • Instance methods:
    • deposit and withdrawal would let the person use their account

Static initializer blocks

Classes don’t have a constructor-style approach that you can use to prepare the class variables for use. However, there is a static form of the intializer block:

A static initializer
import groovy.json.JsonSlurper

class Configuration {
    static String databaseName = ''
    static String databasePassword = ''
    static String logFile = ''

    static {
        //This is the config file:
        File file = new File('config.json')

        //We use JsonSlurper to read a JSON file:
        JsonSlurper slurper = new JsonSlurper()

        //Now parse the config file
        def config = slurper.parse(file)

        //We can access the config file elements using dot-point notation:
        databaseName = config.database
        databasePassword = config.password
        logFile = config.log

    }
}

println """\
    System database: ${Configuration.databaseName}
    System database password: ${Configuration.databasePassword}
    Log file: ${Configuration.logFile}
"""

You’ll see that I’ve just moved the earlier loadConfig class method into a static initializer block (static { }). This is probably a good idea as the initializer block is acted on before the class variables are accessed, allowing me to make sure that the configuration is ready to go rather than relying on other developers to call loadConfig. Additionally, the initializer block will only be called once so the config file is only read once - much more efficient than if loadConfig is called over and over by other code.

  1. More on this in Access Modifiers