Created by: Andrew J. Pounds, Ph.D.

πŸ’» Comparison of Object-Oriented Programming in Java, C++, and D

Welcome to a comprehensive comparison of Object-Oriented Programming (OOP) across three distinct programming languages: Java, C++, and D. This document explores how each language approaches the fundamental concepts of OOP and provides practical examples using the GNU D Compiler (gdc). By understanding these comparisons, you'll develop a deeper appreciation for language design decisions and be better equipped to choose the right tool for your projects.

❓ What is Object-Oriented Programming?

Object-Oriented Programming is a paradigm that organizes code around "objects" that contain both data (variables) and functions (methods). Think of an object like a blueprint: a Drink class defines what properties all drinks have and what operations can be performed on them.

The core principles of OOP are:


πŸ“Š Language Feature Comparison Table

Feature Java C++ D
Constructor ClassName() ClassName() this()
Destructor Automatic (Garbage Collected) ~ClassName() Automatic or ~this()
Inheritance extends : :
Access Modifiers public, private, protected public, private, protected public, private, protected, package
Method Override @Override annotation virtual keyword override keyword
Polymorphism Virtual by default Must use virtual Virtual by default
Memory Management Automatic (Garbage Collection) Manual (new/delete) Automatic or Manual
Interfaces interface keyword Pure virtual classes interface keyword
Constants final keyword const keyword immutable keyword
Compilation javac g++ gdc

✏️ Example 1: Basic Class Definition

Let's compare how to define a basic class in all three languages:

β˜• Java

public class Drink {
    final int MAXIMUM = 10;
    private String name;
    
    public Drink(String brandName) {
        name = brandName;
    }
    
    public String getName() {
        return name;
    }
}

βš™οΈ C++

class Drink {
private:
    static const int MAXIMUM = 10;
    string name;
    
public:
    Drink(string brandName) {
        name = brandName;
    }
    
    string getName() {
        return name;
    }
}

🐲 D

class Drink {
    immutable int MAXIMUM = 10;
    private string name;
    
    this(string brandName) {
        name = brandName;
    }
    
    string getName() {
        return name;
    }
}

Key Observations:


✏️ Example 2: Constructor Overloading

All three languages support multiple constructors with different parameter lists:

β˜• Java

public Drink(String name) {
    this.name = name;
    this.cost = 1.75;
}

public Drink(String name, 
             double cost) {
    this.name = name;
    this.cost = cost;
}

βš™οΈ C++

Drink(string name) {
    this->name = name;
    this->cost = 1.75;
}

Drink(string name, 
      double cost) {
    this->name = name;
    this->cost = cost;
}

🐲 D

this(string name) {
    this.name = name;
    this.cost = 1.75;
}

this(string name, 
     double cost) {
    this.name = name;
    this.cost = cost;
}

Key Observations:


✏️ Example 3: Methods and Encapsulation

Here's how methods work in each language, using the vending machine example:

β˜• Java

public void vend() {
    if (available > 0) {
        available -= 1;
        consumed += 1;
        moneyMade += cost;
        System.out.println(
            name + " Purchased.");
    }
}

public double profit() {
    return moneyMade;
}

βš™οΈ C++

void vend() {
    if (available > 0) {
        available -= 1;
        consumed += 1;
        moneyMade += cost;
        cout << name << 
            " Purchased." << endl;
    }
}

double profit() {
    return moneyMade;
}

🐲 D

void vend() {
    if (available > 0) {
        available -= 1;
        consumed += 1;
        moneyMade += cost;
        writeln(name ~ 
            " Purchased.");
    }
}

double profit() {
    return moneyMade;
}

Key Observations:


πŸ”§ The Complete Vending Machine Example

Now let's look at a complete, working example using the D language and the GNU D Compiler (gdc). This program implements a simple vending machine that tracks drink sales and profits using separate class and driver files.

Important: Selective Module Imports in D

When using D with separate files, the key to successful compilation is using selective imports with the syntax import Module : Symbol;. This explicitly tells the gdc compiler which specific symbol (like a class) you want to import from a module, eliminating namespace ambiguity.

βœ… The Correct Approach: Use import Drink : Drink; in Machine.d to explicitly import only the Drink class from the Drink module. This tells the compiler exactly which symbol you're using and prevents namespace confusion.

Why Selective Imports Are Essential for gdc

The syntax import Module : Symbol; is crucial because:

πŸ’‘ How It Works: The syntax import Module : Symbol; means "From the Module module, bring the Symbol directly into my namespace." This is different from import Module;, which imports everything from the module but maintains the module-qualified namespace (Module.Symbol).

Drink.d - The Drink Class Module

module Drink;

import std.stdio;
import std.math;

class Drink {
    
    // Class constants
    immutable int MAXIMUM_CAPACITY = 10;
    immutable double DEFAULT_DRINK_COST = 1.75;
    
    // Instance variables
    private int available;
    private int consumed;
    private int useDefaultCost;
    private int useSuppliedCost;
    private double drinkCost;
    private double moneyMade;
    private string name;
    
    // Constructor 1: Sets the name of the drink only
    this(string brandName) {
        name = brandName;
        consumed = 0;
        available = 0;
        moneyMade = 0.0;
        drinkCost = DEFAULT_DRINK_COST;
        useDefaultCost = 1;
        useSuppliedCost = 0;
    }
    
    // Constructor 2: Sets name and custom cost
    this(string brandName, double cost) {
        name = brandName;
        consumed = 0;
        available = 0;
        moneyMade = 0.0;
        drinkCost = cost;
        useDefaultCost = 0;
        useSuppliedCost = 1;
    }
    
    // Vend method - dispense a drink if available
    void vend() {
        if (available > 0) {
            available -= 1;
            consumed += 1;
            moneyMade += (useDefaultCost * DEFAULT_DRINK_COST +
                         useSuppliedCost * drinkCost);
            writeln(name ~ " Purchased.");
        } else {
            writeln("************");
            writeln("* SOLD OUT  *");
            writeln("************");
        }
    }
    
    // Refill drinks to maximum capacity
    void refill() {
        available = MAXIMUM_CAPACITY;
    }
    
    // Calculate and return rounded profit
    double profit() {
        return round(moneyMade * 100.0) / 100.0;
    }
    
    // Return number of drinks sold
    int drinksSold() {
        return consumed;
    }
    
    // Get the drink name
    string getDrinkName() {
        return name;
    }
    
    // Return amount needed to restock
    int restockAmount() {
        return MAXIMUM_CAPACITY - available;
    }
}

Machine.d - The Driver Program Module

module Machine;

import std.stdio;
import Drink : Drink;

void main() {
    
    // Create drinks using different constructors
    Drink drink1 = new Drink("Coke");
    Drink drink2 = new Drink("Sprite");
    Drink drink3 = new Drink("Doctor Pepper");
    Drink drink4 = new Drink("V8 Splash", 4.25);
    
    // Refill all drinks
    drink1.refill();
    drink2.refill();
    drink3.refill();
    drink4.refill();
    
    // Purchase 8 Cokes
    drink1.vend();
    drink1.vend();
    drink1.vend();
    drink1.vend();
    drink1.vend();
    drink1.vend();
    drink1.vend();
    drink1.vend();
    
    // Purchase 6 Sprites
    drink2.vend();
    drink2.vend();
    drink2.vend();
    drink2.vend();
    drink2.vend();
    drink2.vend();
    
    // Purchase 2 Doctor Peppers
    drink3.vend();
    drink3.vend();
    
    // Purchase 11 V8 Splash (will exceed capacity)
    drink4.vend();
    drink4.vend();
    drink4.vend();
    drink4.vend();
    drink4.vend();
    drink4.vend();
    drink4.vend();
    drink4.vend();
    drink4.vend();
    drink4.vend();
    drink4.vend();
    
    // Calculate totals
    int totalSold = drink1.drinksSold() + drink2.drinksSold() +
                    drink3.drinksSold() + drink4.drinksSold();
    
    double totalProfit = drink1.profit() + drink2.profit() +
                         drink3.profit() + drink4.profit();
    
    // Display results
    writeln("A Total of ", totalSold, " drinks were sold for a profit of $", totalProfit);
    writeln();
    writeln(drink1.restockAmount(), " ", drink1.getDrinkName(), " needed to restock machine");
    writeln(drink2.restockAmount(), " ", drink2.getDrinkName(), " needed to restock machine");
    writeln(drink3.restockAmount(), " ", drink3.getDrinkName(), " needed to restock machine");
    writeln(drink4.restockAmount(), " ", drink4.getDrinkName(), " needed to restock machine");
}

⚑ Compilation and Execution Instructions

Step-by-Step: Two-File Compilation with gdc (Correct Method)

  1. Create Drink.d with the Drink class code (shown above)
  2. Create Machine.d with the main() function code (shown above)
  3. Save both files in the same directory
  4. Open a terminal and navigate to that directory
  5. Compile both files together in one command:
    gdc Drink.d Machine.d -o machine
  6. Run the program:
    ./machine
βœ… The Key to Success: Use import Drink : Drink; in Machine.d. This selective import syntax explicitly tells gdc that you want to use the Drink class from the Drink module, eliminating any namespace ambiguity.

The Difference Between Import Styles in D

Import Style Syntax Effect Best Used For
Module Import import Drink; Imports all public symbols; requires module qualification (Drink.Drink) When you want to avoid namespace pollution
Selective Import import Drink : Drink; Imports only specified symbols; adds them directly to namespace When you want specific classes available directly (RECOMMENDED)
Aliased Import import D = Drink; Imports module with an alias name (D.Drink) When you have naming conflicts or want shorter names

Sample Output:

Coke Purchased. Coke Purchased. Coke Purchased. Coke Purchased. Coke Purchased. Coke Purchased. Coke Purchased. Coke Purchased. Sprite Purchased. Sprite Purchased. Sprite Purchased. Sprite Purchased. Sprite Purchased. Sprite Purchased. Doctor Pepper Purchased. Doctor Pepper Purchased. V8 Splash Purchased. V8 Splash Purchased. V8 Splash Purchased. V8 Splash Purchased. V8 Splash Purchased. V8 Splash Purchased. V8 Splash Purchased. V8 Splash Purchased. V8 Splash Purchased. V8 Splash Purchased. ************ * SOLD OUT * ************ A Total of 26 drinks were sold for a profit of $67.75 10 Coke needed to restock machine 4 Sprite needed to restock machine 8 Doctor Pepper needed to restock machine 0 V8 Splash needed to restock machine

🎯 Understanding the Code

Module Declarations and Selective Imports

In D, when you split code into multiple files, you use module declarations and can control exactly what you import:

File Declaration Purpose
Drink.d module Drink; Declares that this file is the Drink module
Drink.d import std.stdio; Imports stdio for this module only (regular import)
Machine.d module Machine; Declares that this file is the Machine module
Machine.d import Drink : Drink; Selectively imports the Drink class from Drink module

Why Selective Import Prevents the Error

The error "import 'Machine.Drink' is used as a type" occurred because the compiler was interpreting the Drink symbol as a module import rather than a class. By using selective import syntax import Drink : Drink;, you explicitly tell the compiler:

This removes all ambiguity about what "Drink" refers toβ€”it's clearly the class, not the module.

Class Variables vs. Instance Variables

Aspect Class Variables Instance Variables
Declaration immutable int MAXIMUM_CAPACITY = 10; private int available;
Scope Shared by all instances Unique to each instance
Memory One copy for the whole class One copy per object
Example All drinks have max capacity of 10 Each drink tracks its own inventory

Encapsulation in Action

Notice that in the Drink class, all data members are declared private. This means they cannot be accessed directly from outside the class. Instead, users must use the public methods:

βœ… This is encapsulation: The internal details (how money is tracked, how inventory works) are hidden, and users interact through well-defined methods.

❌ Common Mistakes and Solutions

Mistake 1: Using Module Import Instead of Selective Import

❌ Wrong (Machine.d):
module Machine;

import Drink;  // Regular module import - causes ambiguity!

void main() {
    Drink drink1 = new Drink("Coke");  // ERROR: "import is used as a type"
}
βœ… Correct (Machine.d):
module Machine;

import Drink : Drink;  // Selective import - resolves ambiguity!

void main() {
    Drink drink1 = new Drink("Coke");  // Works correctly
}

Mistake 2: Forgetting the Module Declaration

❌ Wrong (Drink.d):
import std.stdio;

class Drink {
    // Class code...
}
βœ… Correct (Drink.d):
module Drink;

import std.stdio;

class Drink {
    // Class code...
}

Mistake 3: Forgetting to Import the Class in Main

❌ Wrong (Machine.d):
module Machine;

import std.stdio;

void main() {
    Drink drink1 = new Drink("Coke");  // ERROR! Drink not found
}
βœ… Correct (Machine.d):
module Machine;

import std.stdio;
import Drink : Drink;  // Must import the Drink class!

void main() {
    Drink drink1 = new Drink("Coke");
}

Mistake 4: Compiling Only One File

❌ Wrong:
gdc Machine.d -o machine  // ERROR! Missing Drink.d
βœ… Correct:
gdc Drink.d Machine.d -o machine  // Include ALL source files

Mistake 5: Module Name Not Matching File Name

❌ Wrong (file: Drink.d):
module beverage;  // ERROR! Module name should match filename

class Drink {
    // ...
}
βœ… Correct (file: Drink.d):
module Drink;  // Module name matches filename

class Drink {
    // ...
}

Mistake 6: Understanding the "import is used as a type" Error

❌ This Error Means:
Machine.d:9:11: error: import 'Machine.Drink' is used as a type
    9 |     Drink drink1 = new Drink("Coke");
      |           ^

This error occurs when you use import Drink; (regular module import) instead of import Drink : Drink; (selective import). The compiler interprets "Drink" as an import statement/module reference rather than as a class name, causing the ambiguity.

βœ… The Solution:

Always use selective import syntax when importing classes from other modules: import ModuleName : ClassName;. This explicitly tells the compiler that you want to use the class, not just reference the module.


✏️ Your Programming Exercise

Objective: Modify the vending machine program to replace the V8 Splash drink with a new beverage of your choice.

Assignment:

The current program includes V8 Splash at $4.25 as the fourth drink. Your task is to:

  1. Replace V8 Splash: Change the drink name to something different (e.g., "Orange Juice", "Apple Cider", "Iced Tea", "Lemonade", etc.) and choose a new price (different from $4.25)
  2. Modify the vending operations: Change how many times each drink is purchased by modifying the number of vend() method calls for each drink
  3. Update the output display: Ensure all the drink names and restock amounts are printed correctly
  4. Test your program: Compile with gdc and run it to ensure it works properly

Specific Changes to Make:

Testing Your Solution:

  1. Make your changes to Machine.d (or Drink.d if needed)
  2. Save both files
  3. Compile both files together:
    gdc Drink.d Machine.d -o machine
  4. Run:
    ./machine
  5. Verify the output shows your new drink name and the correct profit calculations
🎯 Challenge: Before running your program, calculate what the total profit should be. Does your output match your prediction?

⭐ Key Takeaways About Object-Oriented Programming


πŸ† Language Strengths Summary

Language Strengths Best For
Java Write once, run anywhere; Strong ecosystem; Great for large projects Enterprise applications, web backends, Android apps
C++ High performance; Maximum control; Close to hardware System software, games, performance-critical apps
D Combines best of Java and C++; Modern features; Fast compilation Systems programming, learning OOP, rapid development