Skip to content

Instantly share code, notes, and snippets.

@mgp
Created June 19, 2015 01:51
Show Gist options
  • Save mgp/c5b5a264916fe5c0346e to your computer and use it in GitHub Desktop.
Save mgp/c5b5a264916fe5c0346e to your computer and use it in GitHub Desktop.
Android development best practices

Prefer Immutability

Our principal goal as programmers is to reduce complexity. At any point in a program, we can reason about the state of a constant trivially -- its state is the state upon assignment. By contrast, the state of a variable can change endlessly.

With judicious use, immutable objects can lead to quicker execution speed and lower memory usage. The hash value of a String, the query parameters of an immutable URL, and the distance traced by an immutable sequence of points are all immutable as well. We can cache their values for later use instead of recompute them on every access. We can also share an immutable object with clients without requiring those clients to defensively copy it.

An immutable value is safe in many ways. For example, String is immutable and so its hashCode value is immutable. Consequently, it is safe to use as a String as a key in a Map: The lower order bits of that hash will never change, and so an inserted key will never reside in the wrong bucket. An immutable value is also thread-safe, because a client must acquire a lock only to read data that another client can concurrently modify. An immutable value renders that moot.

A good approach for creating immutable types is to define types that are simply data, and which do not have behavior. For example, consider the SecondsWatched class, which tracks the seconds watched values in a video:

public class SecondsWatched {
    public final long lastSecondWatched;
    public final long totalSecondsWatched;

    public SecondsWatched(long lastSecondWatched, long totalSecondsWatched) {
        /* Preconditions checks here... */

        this.lastSecondWatched = lastSecondWatched;
        this.totalSecondsWatched = totalSecondsWatched;
    }

    /* Method equals, hashCode, and toString follow here. */
}

Note that this instance is immutable because its lastSecondsWatched and totalSecondsWatched fields are final, and hence cannot be reassigned.

The VideoUserProgress instance composes a SecondsWatched instance of this class:

public final class VideoUserProgress extends ContentItemUserProgress {
    public final SecondsWatched secondsWatched;
    public final Optional<Date> lastWatchedDate;

    /* Constructor, equals, hashCode, and toString follow here. */
}

Again, SecondsWatched is immutable. So is an instance of the Date class. A constructed Optional cannot be reassigned to refer to another (possibly null) value, and so it is immutable as well. By virtue of the VideoUserProgress composing instances of immutable classes, and making those fields final, then each VideoUserProgress instance is immutable as well.

Note that for such immutable classes, we do not provide getter methods for each field. Instead, these fields are have public visibility. This is because getter methods are typically paired with corresponding setter methods for access control, but immutable values can by definition not be set.

Use Google Guava

From the Google Guava web page:

The Guava project contains several of Google's core libraries that we rely on in our Java-based projects: collections, caching, primitives support, concurrency libraries, common annotations, string processing, I/O, and so forth.

Some of the packages are indispensable for disciplined Java development:

Preconditions

The Preconditions class is found in the com.google.common.base package. Use it in every constructor you define to ensure that a client is constructing a valid instance. By catching such invalid data when the instance is created, we can determine the source of the invalid parameters by navigating the stacktrace. If we checked for validity upon use, we not only put the burden on every client to ensure that the data is valid, and this precludes finding where the invalid data comes from.

Method checkNotNull

The checkNotNull method accepts a parameter and throws an NullPointerException if it is null. It returns that parameter if it is not. Use it for objects:

public final class ApiClient {
    public final ContentApi contentApi;
    public final UserApi userApi;

    public ApiClient(ContentApi contentApi, UserApi userApi) {
        this.contentApi = Preconditions.checkNotNull(contentApi);
        this.userApi = Preconditions.checkNotNull(userApi);
    }
}

Therefore a client of an ApiClient instance can be ensured that it has contentApi and userApi fields that are not null. It does not need to defend itself against these conditions.

Method checkArgument

The checkArgument method asserts that its expression is true. If not, it throws an IllegalArgumentException. It's best to provide a second parameter that provides a more detailed message for the IllegalArgumentException. This message should include any parameter in the expression that evaluated as false. This makes debugging easier. For example:

public final class SecondsWatched {
    public final long lastSecondWatched;
    public final long totalSecondsWatched;
  

    public SecondsWatched(long lastSecondWatched, long totalSecondsWatched) {
        Preconditions.checkArgument(lastSecondWatched >= 0,
                "lastSecondWatched cannot be negative: " + lastSecondWatched);
        Preconditions.checkArgument(totalSecondsWatched >= 0,
                "totalSecondsWatched cannot be negative: " + totalSecondsWatched);

        this.lastSecondWatched = lastSecondWatched;
        this.totalSecondsWatched = totalSecondsWatched;
    }
}

Object utilities

For immutable objects that are just data and define no behavior, it's useful to implement equals, hashCode, and toString.

Implementing equals

The template for implementing equals is:

public final class VideoSubtitle {
    public final long timeMillis;
    public final String text;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof VideoSubtitle)) return false;

        VideoSubtitle that = (VideoSubtitle) o;

        return (timeMillis == that.timeMillis
                && text.equals(that.text));
    }
}

As shown above, do not forget to use the equals method to compare instance fields that are objects (i.e. extend Object). Accidentally using == will test for identity, or whether both operands refer to the same object (i.e. the same instance at the same address in memory).

Defining an equals method is especially useful for testing, as it allow us to leverage the assertEquals method:

long expectedTimeMillis = 123;
String expectedText = "Let's count to three."
VideoSubtitle expectedSubtitle =
        new VideoSubtitle(expectedTimeMillis, expectedText);

assertEquals(expectedSubtitle, actualSubtitle);

If these values are not equal, then assertEquals prints both arguments in the thrown assertion. (Which is only helpful if toString is implemented, as described below!)

Implementing equals with inheritance

If there is a base class, then the derived class equals implementation should call the base class equals implementation. For example, consider the abstract base class ContentItemUserProgress:

public abstract class ContentItemUserProgress {
    public final ContentItemIdentifier contentItemIdentifier;
    public final UserProgressLevel progressLevel;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof ContentItemUserProgress)) return false;

        ContentItemUserProgress that = (ContentItemUserProgress) o;

        return (contentItemIdentifier.equals(that.contentItemIdentifier) &&
                progressLevel == that.progressLevel);
    }
}

The subclass VideoUserProgress tests that the instance fields in the base class are equal before testing that its own instance fields are equal:

public final class VideoUserProgress extends ContentItemUserProgress {
    public final SecondsWatched secondsWatched;
    public final Optional<Date> lastWatchedDate;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof VideoUserProgress)) return false;

        if (!super.equals(o)) return false;

        VideoUserProgress that = (VideoUserProgress) o;

        return (secondsWatched.equals(that.secondsWatched) &&
                lastWatchedDate.equals(that.lastWatchedDate));
    }
}

Implementing hashCode

The Objects method from the com.google.common.base package contains a hashCode method that computes a hash value for a list of arguments. Use it to compute the hash code of all the instance fields of a class:

@Override
public int hashCode() {
    return Objects.hashCode(lastSecondWatched, totalSecondsWatched);
}

Implementing hashCode with inheritance

If there is a base class, then the derived class hashCode implementation should call the base class hashCode implementation. For example, again consider the base class ContentItemUserProgress:

public final class VideoUserProgress extends ContentItemUserProgress {
    public final SecondsWatched secondsWatched;
    public final Optional<Date> lastWatchedDate;

    @Override
    public int hashCode() {
        return Objects.hashCode(contentItemIdentifier, progressLevel);
    }
}

The subclass VideoUserProgress mixes in the superclass hashCode value into the hashCode value that it returns:

public final class VideoUserProgress extends ContentItemUserProgress {
    public final SecondsWatched secondsWatched;
    public final Optional<Date> lastWatchedDate;

    @Override
    public int hashCode() {
        return Objects.hashCode(super.hashCode(), secondsWatched, lastWatchedDate);
    }
}

Implementing toString

The MoreObjects class from the com.google.common.base package has a useful toStringHelper method that, as its name implies, helps construct a toString value. Again, consider the SecondsWatched class:

public class SecondsWatched {
    public final long lastSecondWatched;
    public final long totalSecondsWatched;

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
                .add("lastSecondWatched", lastSecondWatched)
                .add("totalSecondsWatched", totalSecondsWatched)
                .toString();
    }
}

If SecondsWatched is instantiated with a lastSecondWatched value of 5 and a totalSecondsWatched value of 10, then invoking its toString method will return:

SecondsWatched={lastSecondWatched=5, totalSecondsWatched=10}

Implementing toString with inheritance

If there is a base class, it may be helpful for it to provide a protected factory method that creates a ToStringHelper instance and adds to it the base class fields. The derived class implementation of toString can then invoke this base class factory method and add to it the derived class fields before returning. For example, again consider the base class ContentItemUserProgress:

public abstract class ContentItemUserProgress {
    public final ContentItemIdentifier contentItemIdentifier;
    public final UserProgressLevel progressLevel;

    protected MoreObjects.ToStringHelper getToStringHelper() {
        return MoreObjects.toStringHelper(this)
                .add("contentItemIdentifier", contentItemIdentifier)
                .add("kind", getItemKind())
                .add("progressLevel", progressLevel);
    }
}

The subclass VideoUserProgress then adds its own fields to the ToStringHelper before returning:

public final class VideoUserProgress extends ContentItemUserProgress {
    public final SecondsWatched secondsWatched;
    public final Optional<Date> lastWatchedDate;

    @Override
    public String toString() {
        return getToStringHelper()
                .add("secondsWatched", secondsWatched)
                .add("lastWatchedDate", lastWatchedDate)
                .toString();
    }
}

Collections

TODO(mgp)

@alangpierce
Copy link

Nit: java.util.Date is mutable. It's generally seen as one of the various irreversible mistakes in the Java standard library.

Also, shouldn't we just have Android Studio generate equals, hashCode, and toString for us? In the equals case, it generates almost the same code, and uses getClass comparison rather than instanceof (which is correct because value types and inheritance generally don't mix).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment