6

I'm trying to understand how to properly "extend" 3rd party libraries (with both classes and interfaces) in Java and how to simply use them as drop-in replacements.

I'm currently using the Twitter4J library. Right now I have to manually prepend @ to the result of getScreenName() method of User every time I call it:

twitter4j.User user = getTwitterUser();
String userHandle   = "@" + user.getScreenName();

I've currently tried a few things, but nothing works. This is what I've come up with:

package my.app;

public abstract class User implements twitter4j.User {
    public String getHandle() {
        return "@" + getScreenName();
    }
}

calling it like this:

User user = (User) getTwitterUser();
String userHandle = user.getHandle();

This crashes with the message:

java.lang.ClassCastException: twitter4j.UserJSONImpl cannot be cast to my.app.User
Sheharyar
  • 73,588
  • 21
  • 168
  • 215
  • Would you add more details of the `ClassCastException` you get? For example which class can not be cast to your `my.app.User`? – STaefi Apr 30 '16 at 07:31
  • Shouldn't you extend twitter4j.user and override the getHandle() method rather than implementing it in a abstract class? – user2494817 Apr 30 '16 at 07:32
  • Yes, one of the things I'm most confused about. Since it's not a class, but an interface (without method bodies) how does it return values right now. – Sheharyar Apr 30 '16 at 07:34

3 Answers3

6

Java objects work somewhat like in the real world.

Let's make a car analogy. You have a garage next to your place that sells cars. So you can get a car from the garage:

Car car = garage.buyCar(money);

The cars that the garage happens to sell are Volvos. So, you get a car, and this car (Car is an interface) is actually a Volvo. If you know that the car is a Volvo, you can thus do that:

Volvo car = (Volvo) garage.buyCar(money);

That doesn't change, at all, the way the garage sells cars, or the types of cars that the garage sells. It's just that, since you know they're volvos, you can cast the result to access Volvo car's specificities.

Note that this works now. But nothing prevents the garage to, later, decide to sell VWs instead of Volvos. If they do that, the cast won't work anymore, and it will be your fault: the garage told you it sells cars, but didn't promise it would be a Volvo.

Now, you would like the garage to produce Porsches instead. Trying to do

Porsche car = (Porsche) garage.buyCar(money);

won't work. Just because you want very hard for the car to be a Porsche won't magically change the Volvo that the garage produces to a Porsche.


You can't extend a library that way

All you can do is take the User and call a utility method that creates a handle from that user. Or to create your own UserWithHandle class that implements User, adds a getHandle() method, and implements all the other methods by delegating to the original, wrapped User:

UserWithHandle u = new UserWithHandle(getTwitterUser())

You could do what you want if the library allowed you to provide a UserFactory implementation, that would replace its way of creating users by your own way. Kind of like if the garage allowed you to provide your own painter.


Doing what you want, i.e. adding a method to an existing class is not possible in Java, but is possible in some languages like Kotlin (which is compatible with Java and runs on the JVM, if you're interested), where they're called extension methods. Basically, these methods are static utility methods that would, for example, take a User as argument and return its handle, but can be called as if they were instance method of User.

Community
  • 1
  • 1
JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
  • Okay, that was a good analogy and I understood that the problem with my code is with the "Casting" part. But I'm still not sure how can I extend an existing library. – Sheharyar Apr 30 '16 at 07:38
  • 1
    You can't. Not that way. All you can do is take the User and call a utility method that creates a handle from that user. Or to create your own UserWithHandle class that implements User, adds a getHandle() method, and implements all the other methods by delegating to the original, wrapped User: `UserWithHandle u = new UserWithHandle(getTwitterUser())`. You could do what you want if the library allowed you to provide a "UserFactory" implementation, that would replace its way of creating users by your own way. Kind of like if the garage allowed you to provide your own painter. – JB Nizet Apr 30 '16 at 07:41
  • Doing what you want, i.e. adding a method to an existing class is not possible in Java, but is possible in some languages like Kotlin, where they're called extension methods: basically, these methods are static utility methods that would, for example, take a User as argument and return its handle, but can be called as if they were instance method of User. – JB Nizet Apr 30 '16 at 07:45
  • Hmmm, makes a lot of sense. Please add the contents of this comment to the answer and I'll mark it as accepted. And yes, this would have been super-easy in a language like Ruby. – Sheharyar Apr 30 '16 at 07:49
2

The reason you are getting the ClassCastException is because you are trying to cast the result of getTwitterUser() to your User instance, while it returns twitter4j.User.

If you want it to work, it's not enough to create your class, you also need to create another flavour of getTwitterUser() that will create it.

something like:

public User getMyTwitterUser() {
  Twitter4j tUser = getTwitterUser();
  User myUser = new User(tUser);// assuming you have a constructor that builds your user from the original one
  return myUser;
}
Nir Levy
  • 12,750
  • 3
  • 21
  • 38
0

If you want to extend a library you have to create a wrapper for it and add methods as per your need. But you cannot add any methods to existing library or framework class. Here shared an example which helps you to understand better.

Interface TwitterUserWrappable {
  String getHandle();
  String getDescription();
}

//use this Wrapper pattern to create methods as per your need

Class TwitterUserWrapper: TwitterUserWrappable {
   private twitter4j.User user;
   
   public TwitterUserWrapper(twitter4j.User twitterUser) {
     this.user = twitterUser;
   }

   //Customize the return as per your need after getting screenName
   @override
   public String getHandle() { 
     return "@" + user.getScreenName();
   }

   //Customize the return as per your need after getting description
   @Override
   public String getDescrption() {
     return user.getDescription() + "Customized"; // you can also customize the return as per your need
   }
}

Class Demo {
 public static void main(String[] args) {
   TwitterUserWrappable user = TwitterUserWrapper(getTwitterUser());
   user.getHandle(); //returns what you expected
 }
}
Gowri Sundar
  • 669
  • 1
  • 7
  • 15