· Programming · 8 min read
Programming Languages Comparison
The Good, The Bad, and The Ugly
Want to see some code comparisons?
Below is a brief description of the langage and a side-by-side look at Java, Kotlin, Python, Go, Rust, and TypeScript, with three illustrative examples per language:
- A nontrivial problem showing ease or pain
- A snippet highlighting its core strength
- A case where it feels cumbersome or inefficient
Languages
Java is a statically-typed, object-oriented language widely used in enterprise settings but often criticized for boilerplate and verbosity
Kotlin builds on Java’s ecosystem with concise syntax, first-class coroutines, and extension functions to reduce ceremony
Python is a dynamically-typed language celebrated for expressiveness—especially list comprehensions—but hampered by the Global Interpreter Lock (GIL) for CPU-bound concurrency
Go is a statically-typed, compiled language known for its simple syntax, fast compile times, and built-in CSP-style concurrency via goroutines and channels
Rust guarantees memory safety without a garbage collector through its ownership and borrowing model, enabling zero-cost abstractions
TypeScript layers static types, interfaces, and generics on JavaScript, improving DX and code robustness in large front-end codebases
Java
- Complex Function Example: BigInteger Factorial
Computing large factorials requires Java’s BigInteger, which supports arbitrary-precision arithmetic but entails verbose APIs and object creation overhead.
import java.math.BigInteger;
public class Factorial {
public static BigInteger factorial(int n) {
BigInteger result = BigInteger.ONE;
for (int i = 2; i <= n; i++) {
result = result.multiply(BigInteger.valueOf(i));
}
return result;
}
public static void main(String[] args) {
int number = 30;
System.out.println(number + "! = " + factorial(number));
}
}
- Language Strength Example: Streams API
Java 8’s Streams let you build lazy, possibly parallel data pipelines in a few lines, replacing verbose loops with declarative filter-map-reduce chains.
List<Widget> widgets = ...;
int totalWeight = widgets.stream()
.filter(w -> w.getColor() == Color.RED)
.mapToInt(Widget::getWeight)
.sum();
System.out.println("Total weight of red widgets: " + totalWeight);
- Ugly/Inefficient Example: DOM XML Parsing
Using Java’s DOM parser to load large XML documents can consume 10× the file size in memory and requires cumbersome boilerplate for setup and traversal.
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new File("large.xml"));
// then navigate via NodeList, Element, etc., with many casts and method calls
Kotlin
- Complex Function Example: Structured Concurrency
Kotlin’s coroutines make it straightforward to launch and cancel thousands of lightweight tasks, avoiding thread-pool boilerplate.
suspend fun getUserArticleDetails(userId: String): List<ArticleDetails> = coroutineScope {
articleRepo.getArticles(userId)
.filter { it.isPublic }
.map { async { articleRepo.getArticleDetails(it.id) } }
.awaitAll()
}
- Language Strength Example: Data Classes & Extensions
Data classes auto-generate equals, hashCode, toString and copy, and extension functions let you add methods to types without inheritance.
data class User(val name: String, val age: Int)
fun User.isAdult() = age >= 18
fun main() {
val users = listOf(User("Alice", 20), User("Bob", 17))
users.filter(User::isAdult).forEach { println("${it.name} is an adult") }
}
// No getters/setters, hashcode, equals, toString, copy - no boilerplate!
- Ugly/Inefficient Example: Companion Object Boilerplate
Declaring “static” methods in Kotlin requires a companion object, adding an extra nesting compared to Java’s static keyword.
class Logger {
companion object {
fun log(msg: String) { println(msg) }
}
}
fun main() {
Logger.log("Hello")
}
Python
- Complex Function Example: Quicksort via List Comprehensions
Implementing Quicksort in pure Python is trivial with list comprehensions, but recursion depth and interpreter overhead can hamper performance and scalability.
def quicksort(lst):
if len(lst) <= 1:
return lst
pivot = lst[0]
left = quicksort([x for x in lst[1:] if x < pivot])
right = quicksort([x for x in lst[1:] if x >= pivot])
return left + [pivot] + right
- Language Strength Example: List Comprehensions
List comprehensions combine mapping and filtering into a single, readable expression, boosting productivity in data processing.
numbers = [1, 2, 3, 4, 5]
squares = [x * x for x in numbers if x % 2 == 1]
print(squares) # [1, 9, 25]
- Ugly/Inefficient Example: GIL and CPU-Bound Threads
Python’s GIL serializes bytecode execution, so CPU-bound threads see no parallel speedup, forcing use of multiprocessing or C extensions for true parallelism.
import threading
def cpu_task():
count = 0
for i in range(10\*\*7):
count += i
print(count)
threads = [threading.Thread(target=cpu_task) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
# Still runs on a single core due to GIL
Go
- Complex Function Example: Tree Comparison with Channels
Go code can compare two binary trees concurrently by streaming node values over channels, demonstrating clean CSP patterns.
func Walk(t \*tree.Tree, ch chan int) {
if t == nil { return }
Walk(t.Left, ch)
ch <- t.Value
Walk(t.Right, ch)
}
func Same(t1, t2 \*tree.Tree) bool {
ch1, ch2 := make(chan int), make(chan int)
go func() { Walk(t1, ch1); close(ch1) }()
go func() { Walk(t2, ch2); close(ch2) }()
for v1 := range ch1 {
if v1 != <-ch2 { return false }
}
return true
}
- Language Strength Example: Pipelines
Go’s lightweight goroutines and channels power composable pipelines with almost no locking code.
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
for v := range sq(gen(2, 3, 4)) {
fmt.Println(v)
}
}
- Ugly/Inefficient Example: Pre-1.18 Generics Workarounds
Before Go 1.18, lack of generics forced repetitive boilerplate for each container type.
type IntStack []int
func (s *IntStack) Push(v int) {
*s = append(*s, v)
}
func (s *IntStack) Pop() int {
old := *s
x := old[len(old)-1]
*s = old[:len(old)-1]
return x
}
// To support strings, you'd need a separate StringStack type.
Rust
- Complex Function Example: Word Count with Ownership
Reading a file and counting word frequencies leverages Rust’s ownership and borrowing rules to ensure memory safety without GC.
use std::collections::HashMap;
use std::fs;
fn count_words(filename: &str) -> std::io::Result<HashMap<String, usize>> {
let text = fs::read_to_string(filename)?;
let mut counts = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word.to_string()).or_insert(0) += 1;
}
Ok(counts)
}
- Language Strength Example: Pattern Matching
Rust’s exhaustive match on enums enables clear, safe handling of all variants without nulls or exceptions.
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn process(msg: Message) {
match msg {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to {},{}", x, y),
Message::Write(text) => println!("{}", text),
Message::ChangeColor(r, g, b) => println!("Color to #{:02x}{:02x}{:02x}", r, g, b),
}
}
- Ugly/Inefficient Example for Rust: Doubly-Linked List
Implementing a safe doubly-linked list in Rust is famously painful because each node must be both shared and mutable, yet Rust’s ownership model only allows a single compile-time owner per value.
To work around this, you’re forced to wrap nodes in Rc<RefCell<…>>, which pushes borrowing checks to runtime and adds reference-count overhead.
Compared to languages with garbage collection or raw pointers, the resulting code is verbose, error-prone, and riddled with boilerplate just to maintain safety invariants.
These “necessary hacks” include frequent clone(), borrow_mut(), and manual cleanup of strong/weak references.
Moreover, every mutation must jump through interior-mutability gates, making simple list operations several times longer than in Go or Java.
Code Example
use std::rc::Rc;
use std::cell::RefCell;
struct Node<T> {
data: T,
prev: Option<Rc<RefCell<Node<T>>>>,
next: Option<Rc<RefCell<Node<T>>>>,
}
impl<T> Node<T> {
fn new(data: T) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Node {
data,
prev: None,
next: None,
}))
}
}
struct DoublyLinkedList<T> {
head: Option<Rc<RefCell<Node<T>>>>,
tail: Option<Rc<RefCell<Node<T>>>>,
}
impl<T> DoublyLinkedList<T> {
fn new() -> Self {
DoublyLinkedList { head: None, tail: None }
}
fn push_front(&mut self, elem: T) {
let new_node = Node::new(elem);
match self.head.take() {
Some(old) => {
old.borrow_mut().prev = Some(new_node.clone());
new_node.borrow_mut().next = Some(old);
self.head = Some(new_node);
}
None => {
self.tail = Some(new_node.clone());
self.head = Some(new_node);
}
}
}
}
This snippet, adapted from a community tutorial, already spans over a dozen lines for just one insertion method—far more than in GC-based or pointer-based languages.
Why It’s Painful:
Reference Counting & Interior Mutability Every link must be wrapped in Rc<RefCell<…>> (or Arc<Mutex<…>> for thread safety), incurring runtime borrow checks and atomic ref-count updates
Boilerplate for Simple Operations Even a push_front or pop_back requires manually juggling Option, take(), and clone(), versus a few pointer assignments in other languages
Runtime Cost RefCell enforces borrowing rules at runtime; on every borrow_mut() there’s a check—and potential panic—adding overhead unseen in compile-time-checked languages
Cyclic Data Structures Are Hard Creating cycles (as in a circular list) without leaks needs Weak references, further complicating ownership graphs and cleanup logic
In summary, while safe Rust excels at many systems-level tasks, cyclic or doubly-linked structures clash with its ownership guarantees, resulting in code that is far uglier and less efficient than in languages designed around garbage collection or manual pointers.
TypeScript
- Complex Function Example: Conditional Types & Generics
TypeScript’s conditional types let you compute types at compile time, at the cost of intricate syntax.
type Flatten<T> = T extends Array<infer U> ? U : T;
function flatArray<T>(arr: T[]): Flatten<T>[] {
return (arr as any).reduce((acc: any[], val: any) => acc.concat(val), []);
}
- Language Strength Example: Typed React Components
TSX support and interfaces enforce prop contracts in React, catching mismatches before runtime.
import React from 'react';
interface ButtonProps {
label: string;
onClick: () => void;
}
const Button: React.FC<ButtonProps> = ({ label, onClick }) => (
<button onClick={onClick}>{label}</button>
);
- Ugly/Inefficient Example: Verbose Mapped Types
Defining complex mapped or utility types can become hard to read, especially compared to more concise syntax in some other languages.
type PartialWithNew<T> = {
[P in keyof T]?: T[P];
} & { newProp: string };
Hope you enjoyed this comparison!
This set of examples highlights each language’s sweet spot and pain points in real-world coding scenarios.