Java 6 and Mac OS X 10.5
I'm
fairly confident that Apple will be posting a download of Java 6 shortly. One of the MRJ developers gave a talk at
Desktop Matters, and Java 6 is definitely being worked on. But I feel compelled to dispute a
recent post on
John Gruber's blog:
Apple’s decision to abandon (or postpone, for all we know) work on Java 6 was easy. Some of the most important software for the Mac depends on Carbon. No important software for the Mac depends on Java.
Gruber forgets about in-house software, such as the footwear development app I worked on when I was at Nike. One of the major barriers to Mac adoption in business is the dependencies of custom apps. If custom apps are written in Java (which they usually are), and if the Mac has a modern JVM (which it will soon), it's easier to get a MacBook for work.
He also forgets about server-side software. Java is a popular platform for webapps like
Amazon.com and
the Apple Store. Without a modern JVM, the apps cannot be developed or deployed on a Mac.
If the Mac is to remain exclusively a consumer device, Java isn't that important. But to make inroads into the office and the server room, Java for Mac needs to be a priority for both Sun and Apple.
# posted by Jesse Wilson
on Tuesday, October 30, 2007
0 comments
post a comment
Implementing equals() is hard
When modeling an entity that may or may-not have an id assigned, writing the equals method is surprisingly hard. What to do when there's no id? Falling back on equals-by-value is tempting:
public class Product {
private final Id id;
private final String description;
private final Money price;
...
public boolean equals(Object other) {
if (!(other instanceof Product)) {
return false;
}
Product otherProduct = (Product) other;
if (id != null && otherProduct.id != null) {
return id.equals(otherProduct.id);
} else {
return description.equals(otherProduct.description)
&& price.equals(otherProduct.price);
}
}
}
Unfortunately, this doesn't work. The
equals()
method must be reflective, symmetric and transitive. For arbitrary values of
a,
b, and
c, the following tests
* must always pass:
/* reflective */
assertTrue(a.equals(a));
/* symmetric */
assertTrue(a.equals(b) == b.equals(a));
/* transitive */
if (a.equals(b) && b.equals(c)) {
assertTrue(a.equals(c));
}
Transitivity is hard
Unfortunately, our example fails transitivity:
Product a = new Product(5, "Large Pepperoni", Money.usd(11));
Product b = new Product(null, "Large Pepperoni", Money.usd(11));
Product c = new Product(6, "Large Pepperoni", Money.usd(11));
This is a case where
a equals
b and
b equals
c, but
a doesn't equal
c.
Consequences
Without a proper equals method, it's dangerous to use this Product class in Collections. For example, the behaviour of
HashMap.remove(b)
is poorly defined for a HashMap that contains both
a and
c.
The Fix
The equals method should be defined by value or by id, but not either.
# posted by Jesse Wilson
on Tuesday, October 30, 2007
6 comments
post a comment
A simple pattern for avoiding NullPointerExceptions
Imagine you've got some method that sometimes returns a legitimate value, and other times returns the special value
null
to indicate that no legitimate value exists:
public interface StoreLocator {
/**
* Returns a store nearest to {@code address}, or {@code null}
* if there are no stores within delivery range of the address.
*/
PizzaStore findNearbyStore(Address address);
}
Why returning null is annoying
Since it can return
null
, we need to check for that whenever we call the method:
public Receipt createReceipt(Order order) {
...
PizzaStore closestLocation
= storeLocator.findNearbyStore(order.getDeliveryAddress());
if(closestLocation == null) {
throw new IllegalArgumentException("No stores nearby!");
}
if (!closestLocation.equals(order.getStoreAddress())) {
receipt.addMessage("Next time you order a pizza, consider"
+ " our closer %s location", closestLocation);
}
return receipt;
}
Why returning null is dangerous
In another scenario we might forget to check for null, and end up creating corrupt data. The invalid null value might not be discovered for a long time - it might even get persisted in your database. For example:
public Order createOrder(Address toAddress, List<LineItem> lineItems) {
PizzaStore store = storeLocator.findNearbyStore(toAddress);
return new Order(toAddress, lineItems, store);
}
A simple fix - two methods
Provide two methods to cover the two interesting cases: where there's always a legitimate value, and where there's only sometimes a legitimate value:
public interface StoreLocator {
/**
* Returns a store nearest to {@code address}.
* @throws IllegalArgumentException if there are no stores
* within delivery range of the address.
*/
PizzaStore findNearbyStore(Address address);
/**
* Returns a store nearest to {@code address}, or {@code defaultValue}
* if there are no stores within delivery range of the address.
*/
PizzaStore findNearbyStore(Address address, PizzaStore defaultValue);
}
It's simple and it makes for a user-friendly API. It's particularly nice when the defaultValue fits the problem neatly:
public Address getCustomerServiceAddress(Address customerAddress) {
return storeLocator.findNearbyStore(
customerAddress, PizzaStores.HEAD_OFFICE).getAddress();
}
# posted by Jesse Wilson
on Monday, October 29, 2007
1 comments
post a comment
My favourite bookmarklets
A
bookmarklet is a tiny JavaScript program that lives inside a link. Drag each of these to your browser's bookmark bar - they add simple features to every page you visit:
← go to the "previous" page. This works for pages with numbers in the URLs, like Engadget or multi-page articles. From JWZ.
↑ go "up" in the URL hierarchy. In some blogs, this takes you from the current post to the blog itself. From JWZ.
→ go to the "next" page. From JWZ.
μ create a tinyurl for this page.
X-ray examine the CSS path of the elements on this page. This is a fully-featured program that allows you to debug CSS issues. From westciv.
≈ translate the current page into English. From Google.
JS execute arbitrary JavaScript. Use this to prototype your own bookmarklets!
If you find these useful, it's easy to find
lots of other bookmarklets.
# posted by Jesse Wilson
on Monday, October 29, 2007
0 comments
post a comment
Catching unchecked exceptions is fragile
In my project we're having a great discussion about whether to use checked or unchecked exceptions for a particular problem. I've discovered an interesting problem with unchecked exceptions - they're not as robust with respect to refactoring and maintenance.
Here's some code that gracefully handles the unchecked exception called
MappingServiceUnreachableException
:
/**
* @return the directions, or {@code null} if they are unavailable.
*/
public Directions tryToLookupDirections(Address from, Address to) {
try {
MappingSession session = mappingService.openSession();
MappingResponse response = session.request(
new DirectionLookupRequest(from, to));
} catch(MappingServiceUnreachableException e) {
logger.log(Level.WARNING, e.getMessage(), e);
return null;
}
}
The code passes all unit tests written for it. It gets checked in and gets QA testing. But a few weeks later, I discover a performance problem from not closing the session. This is an easy fix:
/**
* @return the directions, or {@code null} if they are unavailable.
*/
public Directions tryToLookupDirections(Address from, Address to) {
MappingSession session = mappingService.openSession();
try {
MappingResponse response = session.request(
new DirectionLookupRequest(from, to));
} catch(MappingServiceUnreachableException e) {
logger.log(Level.WARNING, e.getMessage(), e);
return null;
} finally {
session.close();
}
}
In order to have a reference to
MappingService
in the finally block, I moved the call to
openSession()
outside of the try block. I've inadvertently changed the behaviour when that method throws!
When do find out about my mistake?
- If
MappingServiceUnreachableException
was a checked exception, I'd find out about my blunder as I make it. If I'm using a good IDE, I find out before I even save the file.
- If I have complete test coverage for the exceptional case, I'll find out about my blunder the next time that test is run - probably within a few minutes.
- If I have only sunny-case test coverage, I won't find out about my blunder until the next time that the mapping service is unavailable.
I prefer fail-fast, so in this case checked exceptions are a no brainer. They're not perfect everywhere, but they certainly serve their purpose.
# posted by Jesse Wilson
on Thursday, October 25, 2007
1 comments
post a comment
Ten blogs for your feedreader
I'm a big fan of blog feeds. As a blog author, they give me an audience to whom I can blast my ideas. And as a blog reader, I can grab the latest content from 159 blogs in just a few seconds.
If you're still surfing the web by hand, consider checking out
Google Reader or another feed reader. You get more content with less effort. Adding a site to your feed reader is much better than bookmarking it - it's like TiVo for the web.
To seed your feedreader, here's some of my favourite feeds:
Ars Technica: tech news
Eric Burke: Java observations
Cédric Beust: Java developer productivity
Neal Gafter: the Java language
Stephen Colebourne: Java APIs
John Gruber: the Mac
Daniel Lyons: the Mac, but snarky
Tim O'Reilly: next year's technology
Joel Spolsky: running an small software shop
Raymond Chen: old school Windows
When given the choice,
prefer Atom over RSS. It won't miscode "A & W" as "A & W".
# posted by Jesse Wilson
on Sunday, October 21, 2007
0 comments
post a comment
Don't let consistency rule your APIs
Consistency in API design is great. It makes the API more predictable, easier to learn, and more maintainable. But I believe there's a difference between being consistent
when writing code, and writing code
to be consistent.
Consider this API:
public enum Status {
PENDING,
PREPARATION_IN_PROGRESS,
PREPARED,
DELIVERY_IN_PROGRESS,
DELIVERED;
CANCELLED,
public boolean isFinished() {
return this == CANCELLED || this == DELIVERED;
}
}
This API provides exactly what's necessary for the code that uses it.
But to be consistent, I tend to extrapolate from
isFinished()
and add additional methods like
isStarted()
and
isActive()
. But these methods aren't needed! They're just dead code that needs to be written, documented, tested, and maintained only for the sake of consistency.
Symmetry is a special case of extraneous consistency:
interface Stack<T> {
/**
* Adds {@code value} to the stack.
*/
void push(T value);
/**
* Removes and returns the most recently pushed value in the stack.
* @throws StackEmptyException if the stack contains no elements
*/
T pop();
}
Consistency tells me to create an exception type
StackFullException
and to declare that
push()
throws it when someone pushes more than
Integer.MAX_VALUE
elements. This situation extremely rare! Writing the extra code and the tricky tests isn't worth the effort.
Finally, a familiar example where consistency ruins an API:
public class InputStream {
int read() throws IOException;
int read(byte[] b) throws IOException;
void close() throws IOException;
}
When a call to read() throws an
IOException
, the caller probably wants to recover from the error. But when
close()
throws, there's not much the caller can do about it! As a result of this, I need nested try/catch blocks every time I close a stream:
InputStream in = null;
try {
in = openInputStream();
return parse(in);
} catch(IOException e) {
throw new ApplicationException("Failed to read input", e);
} finally {
try {
if (in != null) {
in.close();
}
} catch(IOException ignored) {
}
}
A better, but less consistent, API might have
close()
return a boolean to indicate whether the operation was successful.
When writing APIs, be consistent and predictable. But don't be consistent just for consistency's sake - that could lead you to write too much or bad code.
# posted by Jesse Wilson
on Tuesday, October 16, 2007
1 comments
post a comment
Don't fly AirTran
AirTran sold my seat out from under me today for my trip from Buffalo to San Francisco...
I made my annual trip back to southwestern Ontario this weekend to attend Oktoberfest in Kitchener, and to see the 'Riders. Jodie and I flew through Buffalo airport to save some bucks - Buffalo is about a 90 minute drive from Jodie's home in Caledonia, Ontario.
Last night we checked in early at the Holiday Inn Express, right next to the Buffalo Airport so I could get to the airport early for my 6:20am flight. And I got here early - about 5:15am! Check-in was easy, and my boarding pass told me in bold to
be at the gate ten minutes before departure.Unfortunately, there were about 500 people in front of me in line for security, which took until almost 6:10am. The other travellers in line with me were anxious about missing their flights but when they asked to be shortcutted to the front, they weren't given permission. Finally I got my shoes scanned for weapons and I sprinted down the terminal to my gate.
When I got to the gate, I was politely told
"Sorry sir, you're too late for your flight" by a man as he scanned the ticket of another passenger.
I pleaded, "But it's 6:10!"
The guy, "But your seat has already been sold. When you weren't here at 6:10, we sold it to someone else."
I pleaded, "But it's still 6:10!"
So now I'm riding "standby" on their 1:53pm flight. That means that they've overbooked me on that flight and I only get to ride if they shaft somebody else.
Don't fly AirTran, they don't value your time.
# posted by Jesse Wilson
on Monday, October 15, 2007
1 comments
post a comment
Airport WiFi thinks my newsreader is a virus
This morning while waiting for a connection at the Atlanta airport, I signed up for $7.95/day WiFi. It's a lot of cash for Internet, but I'm a news junkie and I wanted to sync up on work email, etc.
As usual, when I got online I refreshed all 159 feeds in my MacBook's
newsreader. About a third of the feeds refreshed, and then the web went dark! I opened Safari, and to my horror I got this error message:
Anomalous Behavior Detected - Request Blocked
Your computer was automatically blacklisted (blocked) due to an abnormal amount of activity. It is possible that your computer is infected with a worm or virus.
This is scary. When I paid
Boingo for WiFi, I certainly didn't anticipate being throttled. When I called their 800-number, they told me to wait 10 minutes, at which point the blacklist would be lifted.
I guess this is a good time to purge my old newsfeeds...
# posted by Jesse Wilson
on Friday, October 12, 2007
1 comments
post a comment
Spammy Java
Compare these two code snippets, they both do the same thing.
Minimalist:
public static <T> Comparator<T> reverse(final Comparator<T> forward) {
Preconditions.checkNotNull(forward, "forward");
return new Comparator<T>() {
public int compare(T a, T b) {
return forward.compare(b, a);
}
};
}
Complete:
/**
* Returns a comparator that's the reverse of {@code forward}.
*
* @param forward the forward comparator
* @return the reversed comparator
* @throws NullPointerException if {@code forward} is null
*/
public static <T> Comparator<T> reverse(final Comparator<T> forward) {
return new ReverseComparator<T>(forward);
}
/**
* A comparator that reverses another comparator.
*
* @param <T> the type to compare
*/
private static class ReverseComparator<T> implements Comparator<T> {
private final Comparator<T> forward;
public ReverseComparator(final Comparator<T> forward) {
super();
Preconditions.checkNotNull(forward, "forward");
this.forward = forward;
}
@Override
public int compare(final T a, final T b) {
return forward.compare(b, a);
}
}
Clearly, some people's spamis other people's ham. For your entertainment, here's my views:
Redundant Javadoc: spam. It decreases the signal-to-noise ratio of your docs, and tends to grow out-of-date. I've
ranted about this before.
@throws NullPointerException Javadoc: okay. But I'd prefer to just have package-level or project doc that says "all parameters and return values will not be null unless otherwise specified".
'final' on parameters: spam. I almost never reassign parameter values, so the keyword doesn't say much.
'final' on fields: ham. Immutability is great, and I like to be explicit about it.
Named classes instead of anonymous ones: spam. Named classes require extra code for fields and a constructor. What a waste of code!
@Override: When I override a non-abstract method, I really want to make sure the name doesn't change - ham. But for abstract methods and interface methods, it doesn't pull it's weight for me - spam.
super() with 0 args: spam. Especially when the superclass doesn't have another constructor that takes parameters.
'public' on interface methods: okay, but I don't use it. The problem here is that 'public' implies that some other modifier could be appropriate here.
Disagree? Tell me why in the comments.
# posted by Jesse Wilson
on Monday, October 08, 2007
3 comments
post a comment
Considering Guice: Explicit dependencies
I love
Guice. So much so that lately I've been trying to push it on my friends for their projects.
But my friends are reluctant. They've become accustomed to manually solving the problems that Guice could solve for them. They enjoy wiring up their apps and implementing singletons, factories and double-checked locking. But most significantly, they don't recognize the limitations of their approach. It's very difficult to convince someone to fix a problem that they don't know they have!
Here's how reluctance is usually voiced:
I don't like indirection. I like being able to know exactly where my objects are coming from, and Guice hides that.
Guice is great for some applications, but it isn't appropriate for the one that I'm writing.
In this series, I intend to make the case for Guice. I'll use examples that show a particular feature or benefit. And although many advantages of Guice are quite abstract, but I'll try to be concrete in my examples.
Explicit Dependencies
JavaScript toggle: standard version, guice version
/**
* Guice version.
*/
public class RealOrderService implements OrderService {
private final ReadableDuration maxTravelDuration;
private final Distance maxTravelDistance;
private final Geography geography;
private final AddressBlacklist addressBlacklist;
private final WorkSchedule workSchedule;
@Inject
public RealOrderService(
@Named("MaxTravelDuration") ReadableDuration maxTravelDuration,
@Named("MaxTravelDistance") Distance maxTravelDistance,
Geography geography,
AddressBlacklist addressBlacklist,
WorkSchedule workSchedule) {
this.maxTravelDuration = maxTravelDuration;
this.maxTravelDistance = maxTravelDistance;
this.geography = geography;
this.addressBlacklist = addressBlacklist;
this.workSchedule = workSchedule;
}
public Set<Reason> validate(Order order) {
Set<Reason> reasons = new HashSet<Reason>();
Store store = order.getStore();
if (order.isDelivery()) {
Address deliveryAddress = order.getDeliveryAddress();
Address storeAddress = store.getAddress();
Route route = geography
.calculateRoute(deliveryAddress, storeAddress);
if (!addressBlacklist.isAddressOk(deliveryAddress)) {
List<Note> blacklistNotes = addressBlacklist.getNotes(deliveryAddress);
reasons.add(new Reason(ReasonId.BLACKLISTED_ADDRESS, blacklistNotes));
}
Duration travelDuration = geography
.calculateTravelDuration(route, order.getQuotedArrivalTime());
if (travelDuration.isLongerThan(maxTravelDuration)) {
reasons.add(new Reason(ReasonId.TRIP_DURATION_TOO_LONG, travelDuration));
}
Distance travelDistance = route.getDistance();
if (travelDistance.isLongerThan(maxTravelDistance)) {
reasons.add(new Reason(ReasonId.TRIP_DISTANCE_TOO_LONG, travelDistance));
}
List<User> onDuty = workSchedule.getPeopleOnDuty(
store, order.getQuotedArrivalTime());
List<User> deliveryPersons = selectUsersInRole(onDuty, Role.ORDER_DELIVERY);
if (deliveryPersons.isEmpty()) {
reasons.add(new Reason(ReasonId.NO_DELIVERY_PERSONS_ON_DUTY));
}
}
...
return reasons;
}
...
}
/**
* Standard version.
*/
public class RealOrderService implements OrderService {
private final ReadableDuration MAX_TRAVEL_DURATION
= Minutes.minutes(15).toStandardDuration();
private final Distance MAX_TRAVEL_DISTANCE
= Distance.forKilometers(10);
public Set<Reason> validate(Order order) {
Set<Reason> reasons = new HashSet<Reason>();
Store store = order.getStore();
if (order.isDelivery()) {
Address deliveryAddress = order.getDeliveryAddress();
Address storeAddress = store.getAddress();
Route route = GeographyFactory.getInstance()
.calculateRoute(deliveryAddress, storeAddress);
AddressBlacklist addressBlacklist = AddressBlacklistFactory.getInstance();
if (!addressBlacklist.isAddressOk(deliveryAddress)) {
List<Note> blacklistNotes = addressBlacklist.getNotes(deliveryAddress);
reasons.add(new Reason(ReasonId.BLACKLISTED_ADDRESS, blacklistNotes));
}
Duration travelDuration = GeographyFactory.getInstance()
.calculateTravelDuration(route, order.getQuotedArrivalTime());
if (travelDuration.isLongerThan(MAX_TRAVEL_DURATION)) {
reasons.add(new Reason(ReasonId.TRIP_DURATION_TOO_LONG, travelDuration));
}
Distance travelDistance = route.getDistance();
if (travelDistance.isLongerThan(MAX_TRAVEL_DISTANCE)) {
reasons.add(new Reason(ReasonId.TRIP_DISTANCE_TOO_LONG, travelDistance));
}
List<User> onDuty = WorkScheduleFactory.getInstance().getPeopleOnDuty(
store, order.getQuotedArrivalTime());
List<User> deliveryPersons = selectUsersInRole(onDuty, Role.ORDER_DELIVERY);
if (deliveryPersons.isEmpty()) {
reasons.add(new Reason(ReasonId.NO_DELIVERY_PERSONS_ON_DUTY));
}
}
...
return reasons;
}
...
}
The Guice code is longer because it includes a constructor that takes all dependencies. This is awesome! Now I can easily figure out what an implementation class depends on by reading its API.
For testability
I'm writing a unit test for RealOrderService.
With the Guice version, I immediately know what services the test requires. My IDE and compiler will make sure I fulfill them.
With the standard version, figuring out which factories to prepare is labour-intensive. I have to either read the full source code, or do trial-and-error of running the test. Or I could leave the default factories in place, and my unit test might end up accessing a database or network service.
For maintainability
RealOrderService is used in two different applications: a rich client and a webapp. But I forgot about the rich client when I added a new dependency on InventoryControlService.
With the Guice version, if I have unit test coverage, the bug is caught at compile time. If I don't, it will fail at app startup. Guice has the complete dependency graph so it can fail fast when it discovers a missing dependency.
With the standard version, if I have unit test coverage, the bug might be caught at test time. If I don't, this bug won't be caught until the code is missing dependency is needed. This might be as late as QA, or even production!
In conclusion
By making your dependencies explicit you increase testability, and maintainability of your code. Guice encourages you to declare your dependencies using regular Java constructors.
# posted by Jesse Wilson
on Sunday, October 07, 2007
2 comments
post a comment
Be conservative in what you accept and conservative in what you send
It's tempting to write overly-friendly APIs:
public void setDeliveries(List<Delivery> deliveries) {
if (deliveries == null) {
deliveries = Collections.emptyList();
}
this.deliveries = Collections.unmodifiableList(
new ArrayList<Delivery>(deliveries));
}
Please don't do this:
It masks bugs. The caller might be passing the wrong deliveries object, such as
orderDeliveries
when they intended
this.orderDeliveries
.
It makes your code difficult to maintain. Assume that this method has several callers and you'd like to override it. Now the subclass needs to maintain this exact same behaviour. You'll face similar issues if you introduce an interface or do other refactorings.
Instead, constrain your inputs as tightly as possible:
public void setDeliveries(List<Delivery> deliveries) {
if (deliveries == null) {
throw new NullPointerException("deliveries");
}
this.deliveries = Collections.unmodifiableList(
new ArrayList<Delivery>(deliveries));
}
This code properly explodes on bad input. Code like this is easier to maintain since there are fewer cases to support.
Note that we would have the same behaviour without the explicit null check because
new ArrayList<deliveries>
throws as well. But I'm coding as much for the
reader as for the
compiler so I like to be as intentional as possible.
Finally, consider writing or
adopting a utility class to encourage writing concise preconditions.
# posted by Jesse Wilson
on Monday, October 01, 2007
4 comments
post a comment