· Programming  · 8 min read

Programming Languages Comparison

The Good, The Bad, and The Ugly

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:

  1. A nontrivial problem showing ease or pain
  2. A snippet highlighting its core strength
  3. 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

Languages

Java

  1. 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));
    }
}
  1. 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);
  1. 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
Languages

Kotlin

  1. 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()
}
  1. 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!
  1. 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")
}
Languages

Python

  1. 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
  1. 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]
  1. 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
Languages

Go

  1. 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
}
  1. 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)
    }
}
  1. 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.
Languages

Rust

  1. 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)
}
  1. 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),
    }
}
  1. 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:

  1. 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

  2. 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

  3. 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

  4. 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.

Languages

TypeScript

  1. 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), []);
}
  1. 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>
);
  1. 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.

Back to Posts

Related Posts

View All Posts »