Topic 7: Polymorphism in Java
📖 7 min read · 🎯 intermediate · 🧭 Prerequisites: encapsulation-in-java, android-building-blocks-iii-broadcast-receivers
Why this matters
Here's the thing — once you've learned classes and inheritance, your brain starts asking: "Can I write one block of code that works with a Circle, a Rectangle, and any shape someone adds later?" That's exactly what polymorphism unlocks. Instead of writing separate logic for every type, you call draw() and Java figures out the right version at runtime. This is the pattern powering every RecyclerView adapter you'll ever build in Android — one onBindViewHolder() that handles many different view types cleanly. That's not a trick. That's the language working for you.
What You'll Learn
- Understand what polymorphism means in Java and why it matters in Android development
- Define abstract base classes and concrete subclasses that override behavior
- Use a polymorphic method signature to accept any subclass transparently
- Build a
RecyclerViewadapter that handles multiple item view types via polymorphism
The Analogy
Think of a universal remote control. It has one pressPlay() button — but depending on which device it's pointed at (a TV, a Blu-ray player, a streaming box), the behavior is completely different. You don't buy a new remote for each device; you just point and press. Polymorphism works the same way: you define one common interface (pressPlay(), or in Java draw()), and each concrete class decides what that action actually means for it. Your code stays the same; the behavior shape-shifts based on the real object underneath.
Chapter 1: Polymorphism Basics — The Shape Example
Polymorphism means "many shapes." In Java, it lets you treat an object as an instance of its parent class rather than its actual class. That means you can write a method that accepts a Shape, and it will happily work with a Circle, a Rectangle, or any future subclass you haven't written yet.
Start by defining an abstract base class:
// Base class
public abstract class Shape {
public abstract void draw();
}
Now create concrete subclasses that each provide their own draw() implementation:
// Subclass Circle
public class Circle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a circle");
}
}
// Subclass Rectangle
public class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
}
A helper class can now accept any Shape without knowing which subclass it is:
public class ShapeDrawer {
public void drawShape(Shape shape) {
shape.draw();
}
}
Inside an Android Activity, this translates directly to runtime flexibility:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ShapeDrawer drawer = new ShapeDrawer();
Shape circle = new Circle();
Shape rectangle = new Rectangle();
drawer.drawShape(circle); // Outputs: Drawing a circle
drawer.drawShape(rectangle); // Outputs: Drawing a rectangle
}
}
Key things to notice:
- Both
circleandrectangleare declared as typeShape, not their concrete types. drawShape()doesn't need anif/else— the JVM dispatches to the rightdraw()at runtime.- Adding a new shape (e.g.,
Triangle) requires zero changes toShapeDrawer.
Chapter 2: Polymorphism in Android — Multiple RecyclerView Item Types
Android's RecyclerView is one of the most natural places to apply polymorphism. When a list contains items of different visual types — say, text messages and image messages — polymorphism lets a single adapter manage all of them cleanly.
Step 1: Define the Base Class and Subclasses
// Base class Message
public abstract class Message {
public abstract int getType();
}
// Subclass TextMessage
public class TextMessage extends Message {
private String text;
public TextMessage(String text) {
this.text = text;
}
public String getText() {
return text;
}
@Override
public int getType() {
return 0; // Type identifier for text messages
}
}
// Subclass ImageMessage
public class ImageMessage extends Message {
private String imageUrl;
public ImageMessage(String imageUrl) {
this.imageUrl = imageUrl;
}
public String getImageUrl() {
return imageUrl;
}
@Override
public int getType() {
return 1; // Type identifier for image messages
}
}
getType() is the polymorphic hook — each subclass returns a different integer, which the adapter uses to select the correct layout.
Step 2: Create the RecyclerView Adapter
public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<Message> messages;
public MessageAdapter(List<Message> messages) {
this.messages = messages;
}
@Override
public int getItemViewType(int position) {
return messages.get(position).getType();
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == 0) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_text_message, parent, false);
return new TextMessageViewHolder(view);
} else {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_image_message, parent, false);
return new ImageMessageViewHolder(view);
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
Message message = messages.get(position);
if (holder instanceof TextMessageViewHolder) {
((TextMessageViewHolder) holder).bind((TextMessage) message);
} else {
((ImageMessageViewHolder) holder).bind((ImageMessage) message);
}
}
@Override
public int getItemCount() {
return messages.size();
}
// ViewHolder for text messages
static class TextMessageViewHolder extends RecyclerView.ViewHolder {
private TextView textView;
public TextMessageViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.text_message);
}
public void bind(TextMessage message) {
textView.setText(message.getText());
}
}
// ViewHolder for image messages
static class ImageMessageViewHolder extends RecyclerView.ViewHolder {
private ImageView imageView;
public ImageMessageViewHolder(View itemView) {
super(itemView);
imageView = itemView.findViewById(R.id.image_message);
}
public void bind(ImageMessage message) {
// Load the image using Glide
Glide.with(imageView.getContext())
.load(message.getImageUrl())
.into(imageView);
}
}
}
Notable patterns here:
getItemViewType()delegates to the polymorphicgetType()— the adapter never hard-codes which class it's looking at.onCreateViewHolder()inflates a different layout per type.onBindViewHolder()usesinstanceofto downcast before calling type-specificbind()methods.- Image loading is delegated to Glide, a popular Android image loading library.
Step 3: Wire the Adapter in the Activity
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private MessageAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
List<Message> messages = new ArrayList<>();
messages.add(new TextMessage("Hello, world!"));
messages.add(new ImageMessage("https://example.com/image.jpg"));
adapter = new MessageAdapter(messages);
recyclerView.setAdapter(adapter);
}
}
The List<Message> holds both TextMessage and ImageMessage objects side by side. Neither MainActivity nor MessageAdapter needs to change if a third message type (e.g., VideoMessage) is added — just create the subclass, add it to the list, and the adapter's getItemViewType() dispatches automatically.
Chapter 3: The Architecture at a Glance
classDiagram
class Message {
<<abstract>>
+getType() int
}
class TextMessage {
-String text
+getText() String
+getType() int
}
class ImageMessage {
-String imageUrl
+getImageUrl() String
+getType() int
}
class MessageAdapter {
-List~Message~ messages
+getItemViewType(int) int
+onCreateViewHolder(...) ViewHolder
+onBindViewHolder(...) void
}
class TextMessageViewHolder {
+bind(TextMessage) void
}
class ImageMessageViewHolder {
+bind(ImageMessage) void
}
Message <|-- TextMessage
Message <|-- ImageMessage
MessageAdapter --> Message
MessageAdapter --> TextMessageViewHolder
MessageAdapter --> ImageMessageViewHolder
🧪 Try It Yourself
Task: Extend the messaging app with a VideoMessage type.
- Create a
VideoMessageclass that extendsMessage, stores avideoUrlstring, and returnsgetType() = 2. - Add a
VideoMessageViewHolderinsideMessageAdapterwith abind(VideoMessage)method that logs the URL to Logcat for now. - Update
onCreateViewHolder()andonBindViewHolder()to handleviewType == 2. - In
MainActivity, add aVideoMessage("https://example.com/video.mp4")to the list.
Success criterion: Run the app. The RecyclerView renders text and image rows as before, and Logcat shows Video URL: https://example.com/video.mp4 for the third item — confirming the new type was dispatched correctly without touching Message, TextMessage, or ImageMessage.
// Starter — VideoMessage skeleton
public class VideoMessage extends Message {
private String videoUrl;
public VideoMessage(String videoUrl) {
this.videoUrl = videoUrl;
}
public String getVideoUrl() {
return videoUrl;
}
@Override
public int getType() {
return 2;
}
}
🔍 Checkpoint Quiz
Q1. In Java, what does polymorphism allow you to do that makes code more reusable?
A) Declare multiple constructors in the same class
B) Treat objects of different subclasses as instances of a shared parent type
C) Access private fields from outside a class
D) Compile the same code on multiple platforms
Q2. Given the following snippet, what does drawer.drawShape(circle) print?
Shape circle = new Circle();
ShapeDrawer drawer = new ShapeDrawer();
drawer.drawShape(circle);
A) Drawing a shape
B) Drawing a circle
C) null
D) Compile error — circle is declared as Shape, not Circle
Q3. In MessageAdapter, getItemViewType() calls messages.get(position).getType(). Why is this preferable to checking if (message instanceof TextMessage)?
A) instanceof does not compile in Java
B) It delegates the type decision to each subclass, so adding a new type requires no changes to the adapter's dispatch logic
C) getType() runs faster than instanceof at all times
D) instanceof only works with interfaces, not abstract classes
Q4. You want to add a PollMessage type to the chat app. Which files do you minimally need to create or modify?
A) Only MessageAdapter
B) Only MainActivity
C) Create PollMessage + add a new ViewHolder inside MessageAdapter + update onCreateViewHolder and onBindViewHolder
D) Rewrite Message, TextMessage, and ImageMessage
A1. B — Polymorphism lets you write one method that accepts the parent type, and it works for every current and future subclass without modification.
A2. B — Even though the variable is typed as Shape, the JVM dispatches draw() to Circle's implementation at runtime. This is dynamic dispatch.
A3. B — getType() is an override contract on the subclass itself. Each subclass owns its type identifier, so the adapter's dispatch logic never needs to change when a new subclass is added.
A4. C — You create the new subclass and its ViewHolder, and update the two onXxxViewHolder methods to handle the new type integer. The base Message class, TextMessage, ImageMessage, and MainActivity are untouched.
🪞 Recap
- Polymorphism lets you declare a variable as a parent type (
Shape,Message) while the actual object is a subclass — the JVM calls the right method at runtime. - Abstract base classes enforce a shared contract (e.g.,
draw(),getType()) that every subclass must fulfill. - Android's
RecyclerView.Adapteris a canonical real-world use of polymorphism:getItemViewType()dispatches to subclass logic, keeping the adapter open for extension and closed for modification. - Adding a new subtype (a new message kind, a new shape) requires creating one new class — not editing existing ones.
- Glide handles image loading in
ImageMessageViewHolder, decoupling network I/O from the adapter's structural logic.
📚 Further Reading
- Java Polymorphism — Oracle Docs — the authoritative Java tutorial on runtime dispatch
- RecyclerView with Multiple View Types — Android developer guide on
getItemViewTypepatterns - Glide Image Loading Library — the library used in
ImageMessageViewHolder.bind() - ⬅️ Previous: Android Building Blocks III: Broadcast Receivers
- ➡️ Next: Android Building Blocks IV: Content Providers