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.
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:
| 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 |
Let's compare how to define a basic class in all three languages:
public class Drink {
final int MAXIMUM = 10;
private String name;
public Drink(String brandName) {
name = brandName;
}
public String getName() {
return name;
}
}
class Drink {
private:
static const int MAXIMUM = 10;
string name;
public:
Drink(string brandName) {
name = brandName;
}
string getName() {
return name;
}
}
class Drink {
immutable int MAXIMUM = 10;
private string name;
this(string brandName) {
name = brandName;
}
string getName() {
return name;
}
}
All three languages support multiple constructors with different parameter lists:
public Drink(String name) {
this.name = name;
this.cost = 1.75;
}
public Drink(String name,
double cost) {
this.name = name;
this.cost = cost;
}
Drink(string name) {
this->name = name;
this->cost = 1.75;
}
Drink(string name,
double cost) {
this->name = name;
this->cost = cost;
}
this(string name) {
this.name = name;
this.cost = 1.75;
}
this(string name,
double cost) {
this.name = name;
this.cost = cost;
}
Here's how methods work in each language, using the vending machine example:
public void vend() {
if (available > 0) {
available -= 1;
consumed += 1;
moneyMade += cost;
System.out.println(
name + " Purchased.");
}
}
public double profit() {
return moneyMade;
}
void vend() {
if (available > 0) {
available -= 1;
consumed += 1;
moneyMade += cost;
cout << name <<
" Purchased." << endl;
}
}
double profit() {
return moneyMade;
}
void vend() {
if (available > 0) {
available -= 1;
consumed += 1;
moneyMade += cost;
writeln(name ~
" Purchased.");
}
}
double profit() {
return moneyMade;
}
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.
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.
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.
The syntax import Module : Symbol; is crucial because:
import Drink; (regular import), the compiler can't tell if "Drink" refers to a module or a class. With import Drink : Drink;, it's unambiguousβyou want the Drink class.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).
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;
}
}
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");
}
Drink.d with the Drink class code (shown above)Machine.d with the main() function code (shown above)gdc Drink.d Machine.d -o machine
./machine
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.
| 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 |
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 |
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.
| 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 |
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:
vend() - handles selling a drinkrefill() - restocks the drinksprofit() - calculates earningsdrinksSold() - returns sales countmodule Machine;
import Drink; // Regular module import - causes ambiguity!
void main() {
Drink drink1 = new Drink("Coke"); // ERROR: "import is used as a type"
}
module Machine;
import Drink : Drink; // Selective import - resolves ambiguity!
void main() {
Drink drink1 = new Drink("Coke"); // Works correctly
}
import std.stdio;
class Drink {
// Class code...
}
module Drink;
import std.stdio;
class Drink {
// Class code...
}
module Machine;
import std.stdio;
void main() {
Drink drink1 = new Drink("Coke"); // ERROR! Drink not found
}
module Machine;
import std.stdio;
import Drink : Drink; // Must import the Drink class!
void main() {
Drink drink1 = new Drink("Coke");
}
gdc Machine.d -o machine // ERROR! Missing Drink.d
gdc Drink.d Machine.d -o machine // Include ALL source files
module beverage; // ERROR! Module name should match filename
class Drink {
// ...
}
module Drink; // Module name matches filename
class Drink {
// ...
}
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.
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.
Objective: Modify the vending machine program to replace the V8 Splash drink with a new beverage of your choice.
The current program includes V8 Splash at $4.25 as the fourth drink. Your task is to:
vend() method calls for each drinkMachine.d, change the line:
Drink drink4 = new Drink("V8 Splash", 4.25);
to your new drink with a new price, for example:
Drink drink4 = new Drink("Orange Juice", 3.50);
vend() calls (you can change the numbers of times each is called)gdc Drink.d Machine.d -o machine
./machine
private, public) control what can be accessedthis() for constructors instead of the class nameimmutable for constants instead of finalimport Module : Symbol; to avoid namespace ambiguitygdc Drink.d Machine.d -o machine~ operator, not +writeln() for output (requires `import std.stdio;`)| 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 |