အခန်း ၃ - Random Access Memory, Array, String

Algorithm တွေဟာ data structure ပေါ်မှာ အခြေခံပြီး အလုပ်လုပ်ပါတယ်။ Data Structure တွေဟာ Random Access Memory (RAM) ပေါ်မှာ နေရာယူထားပါတယ်။ ဒါကြောင့် RAM သဘောတရား အခြေခံ ကို အနည်းငယ် နားလည် ထားဖို့ လိုပါတယ်။

RAM Architecture

RAM ကို ကြီးမားသည့် စာတိုက်ပုံး (P.O. BOX) လို့ မြင်ယောင်ကြည့်ပါ။ နောက်ပိုင်း မြန်မာနိုင်ငံက condo တွေမှာ ရှိသလို စာတိုက်ပုံး အကွက်လေး တွေ အများကြီး ရှိပါတယ်။ အကွက် တစ်ကွက် စီ က သူ့ရဲ့ သီးသန့် အိမ်လိပ်စာ အတွက် ပါပဲ။ Computer မှာလည်း Data ကို သိမ်းချင်သည့် အခါမှာ RAM ပေါ်မှာ သွားသိမ်းဖို့ လိုပါတယ်။ တနည်းပြောရင် အခန်းလွတ်နေသည့် address တစ်ခု မှာ သွားသိမ်းရတာပေါ့။ အခန်း တစ်ခန်း စီ အတွက် Address တစ်ခု ဆီ ထားရှိသည့် သဘောပါ။

ဥပမာ သင့်သူငယ်ချင်း ဆီကို စာပို့ချင် သူငယ်ချင်းနာမည် (variable name) သိရုံ နဲ့ မရပါဘူး။ သူ့ အိမ်လိပ်စာ (memory address) သိမှ ပို့လို့ရမှာပါ (memory ပေါ်မှာ တန်ဖိုးသိမ်း)။ Computer မှာ variable တစ်ခု ကြေငြာလိုက်သည် နှင့် RAM မှာ variable နဲ့ memory address ကို ချိတ်ပေးလိုက်သည့် သဘောပါ။

ဥပမာ - Address 100 မှာ တန်ဖိုး 50 ကို သိမ်းမယ် ဆိုပါစို့။ ပုံမှန်အားဖြင့် အောက်ပါဇယားအတိုင်း မြင်ယောင်နိုင်ပါတယ်။

Memory Address (လိပ်စာ) Value (သိမ်းဆည်းထားသော တန်ဖိုး) Description (ရှင်းလင်းချက်)
0x0099 0 အလွတ်
0x0100 50 Integer တန်ဖိုး 50 သိမ်းထားသည့်နေရာ
0x0101 'A' Character 'A' သိမ်းထားသည့်နေရာ
0x0102 0 အလွတ်

(မှတ်ချက် - Memory Address များကို များသောအားဖြင့် Hexadecimal (16 လီစနစ်) ဥပမာ 0x0100 ဖြင့် ပြသလေ့ရှိပါတယ်။)

Random Access ဆိုတာ ဘာလဲ

Random Access ဆိုတာကတော့ လိပ်စာ သိရင် ဘယ်နားမှာ ပဲ ရှိရှိ Access လုပ်လို့ရသည့် သဘောပါ။ ဥပမာ အားဖြင့် ကျွန်တော်တို့ ငယ်ငယ်က ကက်ဆက် ခွေ တွေမှာ သီချင်း နားထောင်ရင် နောက် တစ်ခုကို ကျော်ချင်သည့် အခါမှာ ရှေ့ပိုင်းကို Fast Forward လုပ်ရပါတယ်။ တနည်းပြောရင် တိတ်ခွေက အရှေ့ ပိုင်းကို လိပ်ပြီး ကျော်သည့် သဘောပေါ့။ ဒါကို Sequential Access လို့ ခေါ်ပါတယ်။ ဒါပေမယ့် Youtube , Spotify နဲ့ computer မှာ သီချင်း နားထောင်ရင် ကြိုက်သည့် သီချင်းကို ကျော်လို့ ရသလို ကြိုက်သည့် အချိန်ကို ကျော်လို့ ရပါတယ်။ တနည်းပြောရင် တိကျသည့် နေရာကို တန်းသွားလို့ရတယ်ပေါ့။ ဒါကို Random Access လို့ ခေါ်ပါတယ်။

RAM မှာလည်း Address 0 ကို သွားတာ နဲ့ Address 1000 ကို သွားတာ ကြာချိန် အတူတူပါပဲ။ တနည်းပြောရင် Memory Access ရဲ့ Time Complexity က O(1)O(1) ဖြစ်ပါတယ်။

Stack နှင့် Heap

Stack နဲ့ Heap က အမြဲတန်း ရောထွေးနေတတ်တယ်။ memory အပိုင်း Stack and Heap နဲ့ Data Structure Stack and Heap က မတူပါဘူး။ တစ်ခါတစ်လေ interview တွေမှာ မင်း Stack နဲ့ Heap ကို သိလားလို့ မေးရင် သေချာအောင် ပြန်မေးရတယ်။ Memory က Stack နဲ့ Heap ကို မေးတာလား။ Data Structure က Stack နဲ့ Heap ကို မေးတာလားပေါ့။​ အခုပြောမည့် အကြောင်းကတော့ Memory မှာ Data သိမ်းသည့် Stack နှင့် Heap ပါ။

Java လို Programming ဘာသာစကား အများစုမှာ Program တစ်ခု အလုပ်လုပ်ချိန် (Runtime) မှာ Memory ကို Stack/Heap ဆိုပြီး ရှင်းပြလေ့ရှိပါတယ်။ ဒီ chapter ထဲမှာလည်း နားလည်လွယ်အောင် အဲ့ဒီ model နဲ့ ဆက်ရှင်းပါမယ်။ ဒါပေမယ့် JVM specification က memory layout အတိအကျကို guarantee မပေးပါဘူး။

Stack Memory

Stack ဆိုတာက မြန်မြန် အလုပ်လုပ်နိုင်ပြီး စနစ်တကျ စီစဥ်ထားသည့် သေးငယ် သည့် memory နေရာပါ။

Stack Memory က ဘယ်လို အလုပ်လုပ်သလဲ ဆိုတော့ LIFO , Last In , First Out (နောက်ဆုံး ဝင်တာက အရင်ဆုံးပြန်ထွက်သည်) စနစ် နဲ့ အလုပ်လုပ်ပါတယ်။ စာအုပ် အဆင့်ဆင့် ထပ်ထားသလိုပေါ့။ အသစ်ထပ်တင်ရင် အပေါ်ဆုံးမှာ တင်ရတယ်။ ပြန်ယူသည့် အခါမှာလည်း အပေါ်ဆုံးက စာအုပ်ကို အရင် ယူရတယ်။

Function (Method) တစ်ခုခု ကို ခေါ်လိုက်သည့် အခါ အဲဒီ Function အတွက် temporary နေရာ ဖန်တီးပေးရတယ်လို့ စဉ်းစားနိုင်ပါတယ်။ Conceptually ဒီနေရာထဲမှာ local variable ဥပမာ int x = 10;, double y = 5.5 စသည်တို့နဲ့ object ကို ညွှန်ပြတဲ့ reference value တွေ (int[] arr, String s) ကို သိမ်းထားတယ်လို့ နားလည်နိုင်ပါတယ်။

Function တစ်ခု အလုပ်လုပ် ပြီးသွားတာ သို့မဟုတ် Return ပြန်လိုက်သည့် အခါမှာ Stack Frame တစ်ခုလုံး Memory ပေါ်က အလိုအလျောက် ချက်ချင်း ဖျက်ပစ်လိုက်ပါတယ်။

Stack Memory က နေရာ သေးငယ်တယ်။ သိမ်းထားတာတွေကလည်း Primitive Data နဲ့ reference value တွေပဲ ဖြစ်သည့် အတွက်ကြောင့် အများကြီး မလိုဘူး။ Base case မရှိတဲ့ recursion တွေလိုမျိုး ခေါ်မိသည့် အခါမှာ StackOverflowError ဆိုသည့် ပြဿနာ တက်တတ်ပါတယ်။ တနည်းပြောရင် function တွေကို ဆင့်ကာ ဆင့် ကာ ခေါ်ယူပြီး ပြည့်သွားတာမျိုးပေါ့။

Heap

Heap ဆိုတာ runtime မှာ dynamically allocate လုပ်ထားတဲ့ objects တွေကို သိမ်းထားတယ်လို့ ရှင်းပြလေ့ရှိတဲ့ memory area ဖြစ်ပါတယ်။

Stack လိုမျိုး အစဥ်လိုက် စီနေဖို့ မလိုပါဘူး။ အလွတ်ရှိတဲ့ memory နေရာတွေမှာ allocate လုပ်ပြီး သိမ်းနိုင်ပါတယ်။

Java ကို conceptually ရှင်းမယ်ဆိုရင် Object တွေကို Heap ပေါ်မှာ သိမ်းတယ်လို့ သဘောထားနိုင်ပါတယ်။

ဥပမာ -

String name = "Mg Mg";
Scanner input = new Scanner(System.in);
int[] arr = new int[100];

Custom Class Object များ စသည်တို့ ဖြစ်ပါတယ်။

ဒီနေရာမှာ ရိုးရိုး သင်ကြားရေး model အရ local reference variables တွေက Stack ဘက်မှာ ရှိပြီး Object တွေက Heap ဘက်မှာ ရှိတယ်လို့ မြင်နိုင်ပါတယ်။

ဥပမာ -

String name = "Mg Mg";

ဒီ statement ကို run လိုက်တဲ့အခါ memory ထဲမှာ အောက်ပါအတိုင်း ဖြစ်ပါတယ်။

1. Stack

name ဆိုတဲ့ variable ကို reference variable အနေနဲ့ Stack side မှာ ရှိတယ်လို့ conceptual model နဲ့ မြင်နိုင်ပါတယ်။
ဒီ variable ထဲမှာ Object ကို မသိမ်းပါဘူး။ Object ကို ညွှန်တဲ့ reference value ကိုသာ သိမ်းထားပါတယ်။ Java က ဒီ reference ကို raw memory address အဖြစ် တိုက်ရိုက် မပြပေးပါဘူး။

2. Heap

"Mg Mg" ဆိုတဲ့ String Object ကတော့ Heap side မှာ ရှိတယ်လို့ နားလည်နိုင်ပါတယ်။
Stack ထဲက name variable က Heap ပေါ်က ဒီ Object ကို လှမ်းညွှန် (reference) လုပ်ထားတာ ဖြစ်ပါတယ်။

Conceptually memory ကို အောက်လိုမြင်နိုင်ပါတယ်။

flowchart LR
    subgraph Stack
        A["name<br/>(reference)"]
    end
    subgraph Heap
        B["'Mg Mg'<br/>(String Object)"]
    end
    A --> B

အကယ်၍ reference variable မရှိတော့တဲ့အခါ (ဥပမာ function ပြီးသွားတဲ့အခါ) Stack ပေါ်က variable က ပျက်သွားနိုင်ပါတယ်။ ဒါပေမယ့် Heap ပေါ်က Object ကတော့ ချက်ချင်း မဖျက်ပါဘူး။

Java မှာတော့ အသုံးမပြုတော့တဲ့ Object တွေကို Garbage Collector ဆိုတဲ့ အလိုအလျောက် စနစ်က နောက်ကွယ်ကနေ ရှင်းလင်းပေးပါတယ်။

C/C++ တို့မှာဆိုရင်တော့ programmer က ကိုယ်တိုင် free() သို့မဟုတ် delete ကို ခေါ်ပြီး memory ကို ဖျက်ပေးရပါတယ်။

ဘာကြောင့် Object ကို function ပြီးတာနဲ့ အလိုအလျောက် မဖျက်တာလဲ ဆိုတော့ Heap ပေါ်က Object တစ်ခုကို နေရာအများကြီးကနေ တစ်ပြိုင်တည်း reference လုပ်ထားနိုင်လို့ ဖြစ်ပါတယ်။

Function တစ်ခု ပြီးသွားတာနဲ့ Object ကို ဖျက်လိုက်မယ်ဆိုရင် ကျန်နေတဲ့ အခြား reference တွေက အသုံးပြုတဲ့အခါ crash ဖြစ်နိုင်ပါတယ်။

ဒါကြောင့် Object ကို ဘယ်သူမှ reference မလုပ်တော့တဲ့အခါ (Unreferenced ဖြစ်တဲ့အခါ) မှသာ ဖျက်ပေးရပါတယ်။

Java မှာတော့ Garbage Collector က ဒီအလုပ်ကို အလိုအလျောက် လုပ်ပေးပါတယ်။

Stack နှင့် Heap အတူတကွ လုပ်ဆောင်ခြင်း

ကျွန်တော်တို့ အပေါ်မှာ ပြထားသည့် String ဥပမာ ကို ပြန်ကြည့်ရအောင်။

String name1 = "Mg Mg";
String name2 = "Mg Mg";
String name3 = "Mg Mg";

ဒီ code ကို run လိုက်တဲ့အခါ Java က memory ကို optimize လုပ်တဲ့အတွက် "Mg Mg" ဆိုတဲ့ String literal Object ကို တစ်ခါပဲ ဖန်တီးပါတယ်။ ဒါကို String Pool လို့ခေါ်ပါတယ်။ (မှတ်ချက် - String Pool သည် Heap တစ်ခုလုံးကို ဆိုလိုခြင်းမဟုတ်ဘဲ Heap အတွင်းရှိ သီးခြား subset တစ်ခုသာ ဖြစ်ပါသည်။)

Stack ပေါ်မှာတော့ name1, name2, name3 ဆိုတဲ့ variables တွေ ရှိပါတယ်။ ဒီ variables တွေက reference variables ဖြစ်ပြီး Object ကို မသိမ်းပါဘူး။ Object ကို ညွှန်တဲ့ reference value ကိုသာ သိမ်းထားပါတယ်။

Heap ထဲမှာတော့ "Mg Mg" ဆိုတဲ့ String Object တစ်ခုတည်းပဲ ရှိပါတယ်။

Conceptually memory ကို အောက်လိုမြင်နိုင်ပါတယ်။

flowchart LR
    subgraph Stack
        A["name1<br/>(reference)"]
        B["name2<br/>(reference)"]
        C["name3<br/>(reference)"]
    end
    subgraph Heap
        D["'Mg Mg'<br/>(String Object)"]
    end
    A --> D
    B --> D
    C --> D

ဆိုလိုတာက "Mg Mg" ဆိုတဲ့ Object တစ်ခုတည်းကို reference variable အများကြီးကနေ share လုပ်ပြီး အသုံးပြုနိုင်ပါတယ်

ဒီလို design လုပ်ထားတာက memory usage ကို လျော့ချစေပြီး performance ကိုလည်း ပိုကောင်းစေပါတယ်။

အကယ်၍ Stack ပေါ်က variable တစ်ခုခု ပျက်သွားတယ်ဆိုရင် (ဥပမာ function ပြီးသွားတဲ့အခါ) ကျန်နေတဲ့ reference တွေ ရှိနေသေးတဲ့အတွက် Heap ပေါ်က Object ကို ချက်ချင်း မဖျက်ပါဘူး။

Java မှာတော့ GC roots တွေကနေ မရောက်နိုင်တော့တဲ့ Object (unreachable object) ဖြစ်သွားတဲ့အခါမှာ GC က ရှင်းလင်းနိုင်တဲ့ candidate ဖြစ်လာပါတယ်။

ဒါပေမယ့် unreachable ဖြစ်သွားတာနဲ့ ချက်ချင်း ဖျက်မယ်လို့ မဆိုလိုပါဘူး။ Garbage Collector က ဘယ်အချိန် run မလဲဆိုတာကို collector ရဲ့ heuristics နဲ့ runtime အခြေအနေပေါ် မူတည်ပြီး ဆုံးဖြတ်တာ ဖြစ်ပါတယ်။

တနည်းဆိုလျှင် Heap နဲ့ Stack ကလည်း အတူ တကွ တွဲပြီး အလုပ်လုပ်တာကို မြင်နိုင်ပါတယ်။

Function Argument Passing: String vs Object

Stack နဲ့ Heap ကို နားလည်ပြီးရင် Function ထဲကို variable တွေ ပို့လိုက်တဲ့အခါ ဘာကြောင့် တချို့ case တွေမှာ value မပြောင်းသလို မြင်ရပြီး တချို့ case တွေမှာ ပြောင်းသွားသလဲ ဆိုတာကို ဆက်ကြည့်လို့ ရပါတယ်။

Java မှာ pass-by-value ပဲ ရှိပါတယ်။ ဒါက အရေးကြီးဆုံး rule ပါ။

အဓိက မှတ်ထားရမယ့် အချက်က Java က original variable ကို မပို့ပါဘူး။ copy တစ်ခုကိုပဲ ပို့တာ ဖြစ်ပါတယ်။

String ကို Function ထဲပို့တဲ့အခါ

ဥပမာ -

void updateName(String name) {
    name = "HELLO";
}

String name = "good";
updateName(name);
System.out.println(name);

Output က good ပဲ ဖြစ်ပါတယ်။

ဘာကြောင့်လဲဆိုတော့ Function ထဲက name ဟာ caller ထဲက name မဟုတ်ဘဲ reference copy အသစ် ဖြစ်ပါတယ်။ အစပိုင်းမှာ နှစ်ခုလုံးက "good" ကိုပဲ ညွှန်နေပါတယ်။

flowchart LR
    subgraph Caller Stack
        A[name]
    end
    subgraph Callee Stack
        B[param name]
    end
    subgraph Heap
        C["String: good"]
    end
    A --> C
    B --> C

Function ထဲမှာ

name = "HELLO";

လို့ ရေးလိုက်တဲ့အခါ "good" String object ကို မပြင်ပါဘူး။ Function ထဲက local reference copy ကို "HELLO" ဆိုတဲ့ String object အသစ်ကို ညွှန်အောင် ပြောင်းလိုက်တာပါ။

flowchart LR
    subgraph Caller Stack
        A[name]
    end
    subgraph Callee Stack
        B[param name]
    end
    subgraph Heap
        C["String: good"]
        D["String: HELLO"]
    end
    A --> C
    B --> D

Function ပြီးသွားတာနဲ့ Callee Stack ပေါ်က param name ပျက်သွားပြီး Caller ထဲက name ကတော့ "good" ကိုပဲ ဆက်ညွှန်နေပါတယ်။ ဒါကြောင့် output က good ဖြစ်တာပါ။

Object ကို Function ထဲပို့တဲ့အခါ

ဥပမာ -

class User {
    String name;
}

void update(User user) {
    user.name = "HELLO";
}

User myUser = new User();
myUser.name = "good";

update(myUser);
System.out.println(myUser.name);

ဒီ code ရဲ့ output က HELLO ဖြစ်ပါတယ်။

ဘာကြောင့်လဲဆိုတော့ Function ထဲကို ဝင်လာတဲ့ user ဟာ reference copy ဖြစ်ပေမယ့် Caller ထဲက myUser နဲ့ Heap ပေါ်က object တစ်ခုတည်းကိုပဲ ညွှန်နေပါတယ်။

flowchart LR
    subgraph Caller Stack
        A[myUser]
    end
    subgraph Callee Stack
        B[param user]
    end
    subgraph Heap
        C["User Object<br/>name = good"]
    end
    A --> C
    B --> C

Function ထဲမှာ

user.name = "HELLO";

လို့ ရေးလိုက်တဲ့အခါ reference ကို မပြောင်းဘဲ Heap ပေါ်က User object ထဲက name field ကို တိုက်ရိုက်ပြောင်းလိုက်တာ ဖြစ်ပါတယ်။

flowchart LR
    subgraph Caller Stack
        A[myUser]
    end
    subgraph Callee Stack
        B[param user]
    end
    subgraph Heap
        C["User Object<br/>name = HELLO"]
    end
    A --> C
    B --> C

ဒါကြောင့် Function ပြီးသွားပြီးနောက် Caller ထဲက myUser.name ကို print လုပ်ရင် HELLO ထွက်လာတာပါ။

Mutable နှင့် Immutable

ဒီနေရာမှာ လူအများစု ရှုပ်သွားတာက String က Object မဟုတ်ဘူးလား ဆိုတာပါ။ အဖြေက String ကလည်း Object ပဲ ဖြစ်ပါတယ်။ ဒါပေမယ့် String နဲ့ User object က behavior မတူတာက mutable / immutable ကွာလို့ ဖြစ်ပါတယ်။

Immutable ဆိုတာ ဘာလဲ

Immutable object ဆိုတာ create လုပ်ပြီးနောက် internal value ကို မပြင်နိုင်တဲ့ object ဖြစ်ပါတယ်။ Value ပြောင်းချင်ရင် object အသစ်ဖန်တီးရပါတယ်။

Java မှာ String က immutable ဖြစ်ပါတယ်။

String s = "good";
s = "HELLO";

ဒီ code မှာ "good" String object ကို မပြင်ပါဘူး။ "HELLO" String object အသစ်ကို ယူညွှန်လိုက်တာ ဖြစ်ပါတယ်။

flowchart LR
    A[s] --> B["String: good"]
    A -.reassign.-> C["String: HELLO"]

တကယ့် memory semantics အရ ပြောရရင် variable s က အရင် "good" ကို ညွှန်နေရာမှ "HELLO" ကို ပြန်ညွှန်သွားတာ ဖြစ်ပါတယ်။ "good" object ကို ကိုယ်တိုင် ပြင်လိုက်တာ မဟုတ်ပါဘူး။

Mutable ဆိုတာ ဘာလဲ

Mutable object ဆိုတာ create လုပ်ပြီးနောက် internal state ကို ပြင်နိုင်တဲ့ object ဖြစ်ပါတယ်။

ဥပမာ custom class တစ်ခု -

class User {
    String name;
}

User u = new User();
u.name = "good";
u.name = "HELLO";

ဒီနေရာမှာ User object အသစ် မဖန်တီးပါဘူး။ Heap ပေါ်က object တစ်ခုတည်းထဲက field value ကို update လုပ်တာပါ။

flowchart LR
    A[u] --> B["User Object<br/>name: 'good' → 'HELLO'"]

Reassign လုပ်တာနှင့် Object ကိုပြင်တာ မတူဘူး

ဥပမာ ဒီ code ကို ကြည့်ပါ -

void update(User user) {
    user = new User();
    user.name = "HELLO";
}

ဒီ code မှာ output မပြောင်းပါဘူး။ ဘာကြောင့်လဲဆိုတော့ user = new User(); က Function ထဲက local reference copy ကို object အသစ်ဆီ ပြောင်းလိုက်တာပဲ ဖြစ်ပါတယ်။ Caller ထဲက myUser reference ကို မထိခိုက်ပါဘူး။

ဒါကြောင့် Java Function parameter တွေကို ရှင်းလင်းစွာ နားလည်ဖို့အတွက် အောက်ပါ rule နှစ်ခုကို မှတ်ထားရင် လုံလောက်ပါတယ်။

  1. Java က pass-by-value ပဲ ရှိတယ်။
  2. reference ကိုပြောင်းတာ နဲ့ reference ညွှန်ထားတဲ့ object ကိုပြောင်းတာ မတူဘူး။

အခြားသော Programming Language များရှိ Memory Management

Stack နဲ့ Heap ကို Java တစ်ခုတည်းကပဲ သုံးတာ မဟုတ်ပါဘူး။ Programming Language အများစုဟာ ဒီ Memory ခွဲဝေမှုစနစ် (Stack & Heap) ကိုပဲ အခြေခံပြီး အလုပ်လုပ်ကြပါတယ်။ ဒါပေမယ့် နောက်ကွယ်ကနေ Memory ကို ဘယ်သူက ဘယ်လို ရှင်းလင်းပေးလဲ (Memory Management Strategy) ဆိုတာပေါ် မူတည်ပြီး ကွာခြားသွားပါတယ်-

  1. Tracing GC ကို အဓိက အသုံးပြုသော Runtime များ (Java, C#, JavaScript):

    • ဒီ runtime တွေမှာ Developer က free() / delete လို memory free code ကို ကိုယ်တိုင် မရေးရပါဘူး။
    • အသုံးမပြုတော့တဲ့ object တွေကို runtime ရဲ့ Garbage Collector (GC) က နောက်ကွယ်ကနေ ရှင်းလင်းပေးပါတယ်။
  2. TypeScript:

    • TypeScript က JavaScript ပေါ်မှာ syntax/type system တင်ထားတာဖြစ်ပြီး ကိုယ်ပိုင် runtime သို့မဟုတ် GC မရှိပါဘူး။
    • ဒါကြောင့် TypeScript program တွေရဲ့ memory behavior က JavaScript runtime (ဥပမာ V8) ပေါ် မူတည်ပါတယ်။
  3. Reference Counting + GC ပေါင်းစပ်အသုံးပြုသော Runtime များ (Python, PHP):

    • Python နဲ့ PHP တို့မှာ object lifetime ကို reference counting နဲ့အဓိက စောင့်ကြည့်ပြီး cycle ဖြစ်နေတဲ့ object တွေအတွက် GC ကို ထပ်မံသုံးပါတယ်။
    • အဲ့ဒါကြောင့် Java/Go လို tracing GC-only model နဲ့ တစ်ပုံစံတည်း မဟုတ်ပါဘူး။
  4. GC + Escape Analysis (Go):

    • Go (Golang) ဟာ GC ကို သုံးတဲ့ ဘာသာစကားဖြစ်ပေမယ့် Escape Analysis ကိုလည်း ပေါင်းစပ်အသုံးပြုပါတယ်။
    • Compile လုပ်တဲ့အချိန်မှာ "ဒီ value က function scope အပြင်ကို ထွက်သွားမလား" ဆိုတာကို ဆုံးဖြတ်ပါတယ်။
    • Escape မလုပ်တဲ့ value တချို့ကို GC ကို မပေးဘဲ stack-like storage ပေါ်မှာပဲ ထားနိုင်တာကြောင့် performance ပိုကောင်းစေပါတယ်။
  5. Manual Memory Management (C, C++):

    • Stack ကို Function ပြီးရင် အလိုအလျောက် ရှင်းပေးတာက အတူတူပါပဲ။
    • ဒါပေမယ့် Heap ပေါ်မှာ နေရာယူထားတဲ့ Memory ကိုတော့ GC က အလိုအလျောက် မရှင်းပေးပါဘူး။ Developer ကိုယ်တိုင် Code ရေးပြီး (free(), delete) မဖြစ်မနေ ပြန်လွတ်ပေးရပါတယ်။ မလွတ်ပေးမိရင် Memory Leak (မှတ်ဉာဏ်မလွတ်ဘဲ ပိတ်ဆို့နေမှု) ဆိုတဲ့ ပြဿနာကြီး ဖြစ်ပါတယ်။
  6. Ownership/Borrowing Model (Rust):

    • Rust ဟာ GC လည်း မသုံးပါဘူး၊ အဲ့ဒီအတွက် မြန်ဆန်ပါတယ်။ ဒါပေမယ့် C/C++ လို ကိုယ်တိုင်လိုက်ဖျက်ပေးစရာလည်း မလိုပါဘူး။
    • Compiler ကနေ Ownership စည်းမျဉ်းတွေ (ဒီ Memory ကို ဘယ်သူက ပိုင်တယ်ဆိုတာ) ကို တင်းတင်းကျပ်ကျပ် သတ်မှတ်ပေးထားပါတယ်။ အဲ့ဒီ ပိုင်ရှင် Variable သက်တမ်းကုန်သွားတာနဲ့ (Out of Scope ဖြစ်တာနဲ့) Heap ပေါ်က Data ကို Compiler ကနေ အလိုအလျောက် Code (Drop) ပြန်ထည့်ပေးပြီး ဖျက်ပစ်ပါတယ်။
    • ဒီစနစ်ကြောင့် dangling pointer လို ပြဿနာတွေကို များစွာ လျော့ချပေးနိုင်ပေမယ့် Memory Leak လုံးဝမဖြစ်ဘူးလို့တော့ မဆိုနိုင်ပါဘူး။ ဥပမာ reference cycle တချို့ကြောင့် leak ဖြစ်နိုင်သေးပါတယ်။

Reference နှင့် Pointer

C/C++ လို ဘာသာစကားတွေမှာ "Pointer" ဆိုတဲ့ စကားလုံးကို ကြားဖူးပါလိမ့်မယ်။ Pointer ဆိုတာ raw memory address ကို ကိုင်ထားပြီး dereference လုပ်ကာ data ကို တိုက်ရိုက် သွားဖတ်/သွားပြင်နိုင်တဲ့ အရာပါ။ Java မှာတော့ Pointer တွေကို တိုက်ရိုက် ကိုင်တွယ်ခွင့် မပေးထားပါဘူး။ အဲဒီအစား Reference ဆိုတဲ့ abstraction နဲ့ object ကို ညွှန်ခိုင်းထားတာ ဖြစ်ပါတယ်။

Pointer ကို တိုက်ရိုက်မြင်ရအောင် Objective-C code နဲ့ အရင်ကြည့်ရအောင်။

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 20;
        int *ptr = &age; // ptr က age ရဲ့ memory address ကို သိမ်းထားတဲ့ pointer

        NSLog(@"age value     = %d", age);
        NSLog(@"age address   = %p", &age);
        NSLog(@"ptr value     = %p", ptr);
        NSLog(@"*ptr value    = %d", *ptr);

        *ptr = 25; // pointer ကနေတစ်ဆင့် age ရဲ့ value ကို ပြင်လိုက်တာ
        NSLog(@"updated age   = %d", age);
    }
    return 0;
}

ဒီ code ထဲမှာ-

ဒါကြောင့် *ptr = 25; လို့ ရေးလိုက်တာနဲ့ age ရဲ့ value ကပါ 25 ဖြစ်သွားပါတယ်။ Pointer က address ကို သိမ်းထားပြီး၊ dereference (*ptr) လုပ်တဲ့အခါ အဲ့ဒီ address ထဲက data ကို တိုက်ရိုက် သွားထိတာပါ။

Java မှာတော့ ဒီလို *ptr, &age လို syntax တွေကို မသုံးရပါဘူး။ ဒါပေမယ့် Object တွေကို ကိုင်တွယ်တဲ့အခါ Reference ဆိုတဲ့ အလားတူ သဘောတရားနဲ့ အလုပ်လုပ်နေပါတယ်။ အရေးကြီးတာက Java reference ကို raw address လို့ မမြင်သင့်ဘဲ JVM က စီမံထားတဲ့ opaque handle တစ်ခုလိုပဲ နားလည်သင့်ပါတယ်။

Java reference ကိုလည်း အောက်က code နဲ့ တိုက်ရိုက် နှိုင်းယှဉ်ကြည့်နိုင်ပါတယ်။

class User {
    String name;
}

public class Main {
    public static void main(String[] args) {
        User user1 = new User();
        user1.name = "Mg Mg";

        User user2 = user1; // object ကို copy ကူးတာ မဟုတ်ဘဲ reference ကိုပဲ copy လုပ်တာပါ။
        user2.name = "Aung Aung";

        System.out.println(user1.name); // Output: Aung Aung
        System.out.println(user2.name); // Output: Aung Aung
    }
}

ဒီနေရာမှာ user1 နဲ့ user2 က variable ၂ ခု ဖြစ်ပေမယ့် conceptual model အရ User object တစ်ခုတည်းကိုပဲ ညွှန်နေပါတယ်။ ဒါကြောင့် user2 ကနေ name ကို ပြင်လိုက်တာနဲ့ user1 ကနေလည်း အဲ့ဒီ ပြောင်းလဲမှုကို မြင်ရတာပါ။

Primitives (ရိုးရိုး Data တန်ဖိုးများ)

Primitive ဆိုတာ Programming Language ကနေ အခြေခံအကျဆုံး ကြိုတင်သတ်မှတ်ထားတဲ့ ရိုးရှင်းတဲ့ Data အမျိုးအစားတွေပါ။ Java မှာ Primitive Types (၈) မျိုး ရှိပါတယ်။ အဲ့ဒါတွေကတော့-

  1. ဂဏန်းပြည့်များ: byte, short, int, long
  2. ဒသမကိန်းများ: float, double
  3. အက္ခရာတစ်လုံး: char
  4. အမှန်/အမှား: boolean

ဒီ Primitive variables တွေဟာ အရွယ်အစား ပုံသေ သတ်မှတ်ထားပြီးသားဖြစ်လို့ local variable အနေနဲ့ သင်ကြားရေး model ထဲမှာ တန်ဖိုးအစစ်ကို တိုက်ရိုက် ကိုင်ထားတယ် လို့ နားလည်နိုင်ပါတယ်။ ဒါပေမယ့် Java implementation အမှန်တကယ်က အမြဲ stack ပေါ်မှာပဲ သိမ်းရမယ်လို့ မဆိုလိုပါဘူး။ ဥပမာ object field သို့မဟုတ် array element အဖြစ် ရှိနေတဲ့ primitive data က heap object အတွင်းမှာလည်း ရှိနိုင်ပါတယ်။

int a = 10;
int b = a; // 'a' ရဲ့ တန်ဖိုး (10) ကို 'b' ထဲ လုံးဝ "မိတ္တူ" ကူးထည့်လိုက်ပါတယ်။
b = 20;    // 'b' ကို ပြင်လိုက်ပေမယ့် 'a' ကတော့ 10 အတိုင်းပဲ ဆက်ရှိနေပါတယ်။ (အချင်းချင်း မသက်ဆိုင်တော့ပါ)
System.out.println(a); // Output: 10
flowchart LR
    subgraph Stack["Stack Memory"]
        direction TB
        v_a["a = 10"]
        v_b["b = 20 <br>(မိတ္တူကူးပြီး သီးခြားပြင်ထား)"]
        
        style v_a fill:#f9f6e3,stroke:#333
        style v_b fill:#f9f6e3,stroke:#333
    end

Reference Types (Objects/Arrays)

Primitive အမျိုးအစား ၈ မျိုးကလွဲရင် ကျန်တဲ့ Data တွေ အားလုံးဟာ Reference Types (ရည်ညွှန်းလိပ်စာများ) ဖြစ်ပါတယ်။

ဒီအမျိုးအစားတွေကို Java teaching model ထဲမှာတော့ object အစစ်ကို heap ဘက်မှာရှိတယ်, variable ကတော့ အဲ့ဒီ object ကို ညွှန်တဲ့ reference value ကို ကိုင်ထားတယ်လို့ ရှင်းပြလေ့ရှိပါတယ်။ ဒါပေမယ့် ဒီ reference value ကို raw memory address အတိအကျလို့ မမြင်သင့်ပါဘူး။ Representation က ကွဲပြားနိုင်ပါတယ်။

int[] arr1 = {1, 2, 3}; // (Heap ပေါ်မှာ Array အစစ်ကြီး သွားဆောက်ပြီး၊ arr1 ထဲမှာ အဲ့ဒီ array ကို ညွှန်တဲ့ reference value ရှိပါတယ်။)
int[] arr2 = arr1;      // (arr1 ထဲက reference value ကို မိတ္တူကူးပေးလိုက်တာပါ။ ဒါကြောင့် Remote Control ၂ ခုက Array အစစ် တစ်ခုတည်းကို ညွှန်ပြနေပါတယ်။)

arr2[0] = 99; // Remote 'arr2' ကိုသုံးပြီး Heap ပေါ်က အစစ်ကြီးရဲ့ ပထမနေရာကို ပြင်လိုက်ပါတယ်။

// arr1 ကလည်း အတူတူပဲ ညွှန်ပြနေတဲ့ Remote ဖြစ်နေတဲ့အတွက် arr1 ရဲ့ တန်ဖိုးပါ ပြောင်းသွားပါတယ်။
System.out.println(arr1[0]); // Output: 99
flowchart LR
    subgraph Stack["Stack Memory"]
        direction TB
        v_arr1["arr1 = (same reference)"]
        v_arr2["arr2 = (same reference)"]
        

    end

    subgraph Heap["Heap Memory"]
        direction TB
        obj_array["Array Object <br> [ 99, 2, 3 ]"]
        
    end

    v_arr1 -.->|Reference 1| obj_array
    v_arr2 -.->|Reference 2| obj_array

(မှတ်ချက် - အပေါ်က diagram က conceptual model သာ ဖြစ်ပါတယ်။ Java program ထဲက reference value ကို raw hex memory address အနေနဲ့ တိုက်ရိုက် မြင်ရတာ မဟုတ်ပါဘူး။)

ဥပမာ

  • Primitives: ခဲတံတစ်ချောင်း ဝယ်တယ်။ သူငယ်ချင်းကို တစ်ချောင်း ထပ်ဝယ်ပေးလိုက်တယ် (မိတ္တူကူးလိုက်တယ်)။ သူငယ်ချင်းက သူ့ခဲတံကို ချိုးပစ်လိုက်ပေမယ့် ကိုယ့်ခဲတံက အကောင်းအတိုင်းပဲ ရှိနေပါတယ်။
  • References: အိမ်ဆောက်ပြီး သော့ တစ်ချောင်း ထုတ်တယ်။ အဲ့ဒီ သော့ကို ပွားပြီး သူငယ်ချင်းကို ပေးလိုက်တယ်။ သူငယ်ချင်းက အဲ့ဒီသော့နဲ့ အိမ်ထဲဝင်ပြီး တီဗွီကို ဖျက်ဆီးလိုက်ရင်၊ ကိုယ်ဝင်ကြည့်တဲ့အခါမှာလည်း တီဗွီက ပျက်စီးနေမှာပါပဲ။ (ဘာလို့လဲဆိုတော့ အိမ်က တစ်လုံးတည်း ဖြစ်နေပြီး နှစ်ယောက်စလုံးက တစ်အိမ်တည်းကို ညွှန်ပြနေလို့ပါ။)

ဒီ Memory Address ညွှန်ပြတဲ့ သဘောတရားကို သေချာနားလည်ရင် Array တွေ ဘာကြောင့် ဒီလို အလုပ်လုပ်တယ်ဆိုတာကို ရှင်းရှင်းလင်းလင်း မြင်လာမှာ ဖြစ်ပါတယ်။

Array ဆိုတာ ဘာလဲ

Array ဆိုတာ Data တွေ တည်ဆောက်သည့်အထဲမှာ အခြေခံ အကျဆုံး နဲ့ အသုံးအများဆုံး Structure တစ်ခုပါ။ သူက Data တွေကို RAM မှာ တစ်ဆက်တည်း သိမ်းဆည်းထားတာ ဖြစ်ပါတယ်။ Array မှာ (မှတ်ချက် - Java မှာ int[] လို primitive array ဆိုရင် တန်ဖိုးတွေကိုယ်တိုင် array object အတွင်းမှာ ဆက်တိုက်ရှိပြီး၊ String[] လို object array ဆိုရင်တော့ object အစစ်တွေ မဟုတ်ဘဲ object တွေကို ညွှန်တဲ့ reference value တွေကသာ ဆက်တိုက်ရှိပါတယ်။ JVM ကလည်း physical RAM ပေါ်က နေရာချထားပုံကို abstraction လုပ်ထားပါတယ်။)

ဆိုပြီးရှိပါတယ်။

Static Array

Static Array ဆိုတာကတော့ ယူမည့် အခန်း အရေအတွက် ကို ကြေငြာပြီးရင် ပြန်ပြင်မရတော့ပါဘူး။ ယူမည့် အခန်းအရေအတွက်ကို အရင် ကြေငြာရပါတယ်။ Java မှာ ဆိုရင် int [] arr = new int[10]; လို့ ရေးပါတယ်။ ထပ်ချဲ့လို့ မရသည့် အတွက် Static Array လို့ ခေါ်ပါတယ်။

Array က ဘာလို့ မြန်တာလဲ ? ဥပမာ Array ရဲ့ အစ (base address) က 1000 ဖြစ်ပြီး int တစ်ခု က 4 bytes ဆီ နေရာယူတယ် ဆိုပါစို့။ arr[5] ၆ ခု မြောက်အခန်း ကို လိုချင်ရင် loop ပတ်ပြီး ရှာနေဖို့ မလိုပါဘူး။

Address = BaseAddress + (Index * DataSize)
Address = 1000 + (5 * 4) = 1020

ဒါဆိုရင် address 1020 ကို တန်းသွားလိုက်တာနဲ့ data ရပါတယ်။ (1000 က ရှင်းအောင်သုံးထားတဲ့ ဥပမာ decimal address ဖြစ်ပြီး၊ တကယ့် memory address တွေကို hexadecimal ဥပမာ 0x1000 နဲ့ ပြသလေ့ရှိပါတယ်။)

ဒါကြောင့် index သိရင် တန်ဖိုးကို ဖတ်တာ (read) နဲ့ ပြင်တာ (write) က O(1)O(1) ပဲ ကြာပါတယ်။ ဒါပေမယ့် ဒီ O(1)O(1) က index နဲ့ တိုက်ရိုက် access/update လုပ်တာ ကိုသာ ဆိုလိုပါတယ်။ နောက်မှာ ပြမယ့် အလယ်တစ်နေရာမှာ ထည့်တာ (insert)ဖျက်တာ (delete) ကတော့ O(1)O(1) မဟုတ်ဘဲ ပိုကြာပါတယ်။

import java.util.Arrays;

public class ArrayExample {
    public static void main(String[] args) {
        // အခန်း ၆ ခန်းပါသော Array တည်ဆောက်ခြင်း (Data ၅ ခုပဲ ထည့်ထားပါမည်)
        int[] numbers = new int[6];
        numbers[0] = 10;
        numbers[1] = 20;
        numbers[2] = 30;
        numbers[3] = 40;
        numbers[4] = 50;
        // numbers[5] သည် တန်ဖိုးမထည့်ရသေးသဖြင့် 0 ဖြစ်နေပါမည်။
        
        System.out.println("Before Insertion: " + Arrays.toString(numbers));
    }
}

Array ၏ Index နှင့် Zero-based

Array ထဲက အခန်းတွေကို ရေတွက်တဲ့အခါ 1 ကနေ မဟုတ်ဘဲ 0 ကနေ စတင်ရေတွက်ပါတယ်။ ဒါကို zero-based indexing လို့ ခေါ်ပါတယ်။ ဒါကြောင့်-

ဘာကြောင့် 0 ကနေ စသလဲဆိုတော့ Index က base address ကနေ ဘယ်လောက် ဝေးတယ်ဆိုတဲ့ offset ဖြစ်လို့ပါ။ ပထမဆုံးအခန်းက base address မှာ တည့်တည့်ရှိတဲ့အတွက် offset က 0 ဖြစ်ပါတယ် (Address = 1000 + (0 × 4) = 1000)။

Array မှာ Insert နှင့် Delete ကုန်ကျစရိတ်

Read/Write က O(1)O(1) ဖြစ်ပေမယ့် Array အလယ်တစ်နေရာမှာ data ထည့် (insert)ဖျက် (delete) ဖို့ဆိုရင်တော့ ဈေးကြီးပါတယ်။ Array က data တွေ တစ်ဆက်တည်း ရှိနေဖို့ လိုတဲ့အတွက်ကြောင့်ပါ။

[10][20][30][40]        index 1 မှာ 15 ထည့်ချင်ရင်
       ↓ (20,30,40 ကို ညာဘက်ရွှေ့)
[10][  ][20][30][40]
[10][15][20][30][40]    ← O(n)

ဒါကြောင့် Array ရဲ့ operation cost ကို အောက်လို မှတ်ထားနိုင်ပါတယ်-

Operation Time Complexity
Index နဲ့ Access (read) O(1)O(1)
Index နဲ့ Update (write) O(1)O(1)
အဆုံးမှာ Insert (append) O(1)O(1) *
အလယ်/အစမှာ Insert O(n)O(n)
အလယ်/အစမှာ Delete O(n)O(n)
Search (value ရှာ) O(n)O(n)

* Append မှတ်ချက်: Static Array ဟာ အရွယ်အစား ပုံသေဖြစ်တဲ့အတွက် သတ်မှတ်ထားတဲ့ length ထက် ကျော်ပြီး တကယ် append လုပ်လို့ မရပါဘူး။ နေရာလွတ် (capacity) ကို ကြိုယူထားပြီး logical size ကို သီးခြား track လုပ်ထားမှသာ အဆုံးမှာ ထည့်တာ O(1)O(1) ဖြစ်ပါတယ်။ ဘယ်လောက်ဆန့်မလဲ ကြိုမသိဘဲ ထည့်ချင်သလောက် ထည့်လို့ရတဲ့ append (O(1)O(1) amortized) ကိုတော့ အောက်က Dynamic Array မှာ ရရှိပါတယ်။

Dynamic Array

Static Array တွေမှာ အခန်းကို ကြိုသတ်မှတ်ရသည့် ပြဿနာ ဖြေရှင်းဖို့ Dynamic Array တွေပေါ်ပေါက်လာပါတယ်။ Java မှာ ArrayList လို့ လူသိများပါတယ်။ သူကတော့ Data တွေကို ထည့်ချင်သလောက် ထည့်လို့ရပြီး Size ကို အလိုလျောက်ညှိပေးသွားပါတယ်။

Dynamic Array အလုပ်လုပ်ပုံကတော့

  1. Initial Capacity : စစချင်းမှာ အခန်း အနည်းငယ် (ဥပမာ ၅ ခန်း) ပါသည့် Array တစ်ခု ကို နေရာယူလိုက်တယ်။
  2. Checking Space: Data အသစ်တစ်ခု ထည့်လိုက်တိုင်း နေရာလောက်သေးလား စစ်တယ်။
  3. Resizing: အကယ်၍ နေရာပြည့်သွားပြီဆိုရင်
    1. အရင် Array ထက် ၂ ဆ ကြီးသည့် Array တစ်ခု ထပ်ဆောက်လိုက်တယ်။
    2. အရင် Array အဟောင်းက Data တွေကို အသစ်ထဲ တစ်ခန်းဆီ Copy လုပ်တယ်။
    3. ပြီးမှ အဟောင်း ကို ဖျက်ချပြီး အသစ်ကို ဆက်သုံးပါတယ်။
flowchart TD
    subgraph Step1[၁။ မူလ Array ပြည့်သွားခြင်း Size: 3]
        direction LR
        A1[10] --- B1[20] --- C1[30]
    end

    subgraph Step2[၂။ ၂ ဆ ကြီးသော Array အသစ်တည်ဆောက်ခြင်း Size: 6]
        direction LR
        A2[Empty] --- B2[Empty] --- C2[Empty] --- D2[Empty] --- E2[Empty] --- F2[Empty]
    end

    subgraph Step3[၃။ Data ဟောင်းများကို ကူးထည့်ခြင်း]
        direction LR
        A3[10] --- B3[20] --- C3[30] --- D3[Empty] --- E3[Empty] --- F3[Empty]
    end
    
    subgraph Step4[၄။ Data အသစ် ထပ်ထည့်ခြင်း]
        direction LR
        A4[10] --- B4[20] --- C4[30] --- D4([40]) --- E4[Empty] --- F4[Empty]
    end

    Step1 --> Step2 --> Step3 --> Step4
import java.util.ArrayList;

public class DynamicArrayExample {
    public static void main(String[] args) {
        // Dynamic Array တည်ဆောက်ခြင်း (Size ကြိုပြောစရာမလို)
        ArrayList<Integer> list = new ArrayList<>();
        
        // Data ထည့်ခြင်း - O(1) [Amortized]
        list.add(10);
        list.add(20);
        list.add(30);
        
        // အလယ်မှာ ထည့်ခြင်း - O(n) [နောက်ကကောင်တွေ ရွှေ့ရလို့]
        list.add(1, 15); 
        
        // Data ဖတ်ခြင်း - O(1)
        System.out.println("Element at index 1: " + list.get(1));
        
        // လက်ရှိ ဘယ်နှစ်ခု ရှိနေပြီလဲ
        System.out.println("Current size: " + list.size());
    }
}

Dynamic Array ဟာ resize ပြန်လုပ်သည့် အချိန်မှာ နှေးသွားနိုင်ပါတယ်။ ဒါကြောင့် size အတိအကျ သိလျှင် Static Array ကို အသုံးပြုသင့်ပါတယ်။

String ဆိုတာ ဘာလဲ

String ဆိုတာ စာလုံး (character) တွေ အစဉ်လိုက် စုထားတာ ဖြစ်ပါတယ်။ Conceptually အားဖြင့် String ကို Array လိုပဲ index နဲ့ access လုပ်လို့ရတဲ့ character/code unit sequence တစ်ခုလို့ မြင်နိုင်ပါတယ်။ ဒါကြောင့် charAt(i) နဲ့ တစ်လုံးချင်းစီကို O(1)O(1) နဲ့ ယူနိုင်ပါတယ်။

(မှတ်ချက် - အတွင်းပိုင်း implementation ကတော့ Java version အလိုက် ကွဲပြားပါတယ်။ ခေတ်သစ် Java မှာ String ကို char[] အစား byte[] (Compact Strings) နဲ့ သိမ်းတာမျိုး ရှိနိုင်ပါတယ်။ ထို့အပြင် charAt() က ပြန်ပေးတဲ့ char က UTF-16 code unit တစ်ခုဖြစ်ပြီး user မြင်ရတဲ့ စာလုံးတစ်လုံးနဲ့ အမြဲ တူချင်မှ တူပါမယ်။ ဥပမာ မြန်မာစာလို script မှာ မြင်ရတဲ့ စာလုံး/syllable တစ်လုံးဟာ code point/code unit များစွာနဲ့ ဖွဲ့စည်းထားနိုင်ပါတယ်။)

String s = "HELLO";
//          H  E  L  L  O
// index    0  1  2  3  4

System.out.println(s.charAt(0)); // H
System.out.println(s.charAt(4)); // O
System.out.println(s.length());  // 5

ဆိုလိုတာက "HELLO" ကို conceptually အောက်လို character array အဖြစ် မြင်နိုင်ပါတယ်-

index :  0    1    2    3    4
char  : 'H'  'E'  'L'  'L'  'O'

String Immutability

အပေါ်က Mutable/Immutable အပိုင်းမှာ ပြောခဲ့သလို Java မှာ String က immutable ဖြစ်ပါတယ်။ ဆိုလိုတာက String တစ်ခု ဖန်တီးပြီးရင် အထဲက character တွေကို တိုက်ရိုက် ပြန်ပြင်လို့ မရပါဘူး။

String s = "HELLO";
// s.charAt(0) = 'J';   // ❌ ဒီလိုလုပ်လို့ မရပါဘူး
s = s.replace('H', 'J'); // String အသစ် "JELLO" ကို ဖန်တီးပြီး s ကို ပြန်ညွှန်တာ

ဒါကြောင့် String ကို ထပ်ခါထပ်ခါ ပေါင်း (concatenate) လုပ်ရင် အကြိမ်တိုင်း String object အသစ် ဖန်တီးနေရပြီး ဈေးကြီးနိုင်ပါတယ်။ String အများကြီး ပြောင်းလဲဖို့ လိုရင် Java မှာ StringBuilder (mutable) ကို သုံးသင့်ပါတယ်။

StringBuilder sb = new StringBuilder();
sb.append("HE");
sb.append("LLO");
String result = sb.toString(); // "HELLO"

Common Operations နှင့် ကုန်ကျစရိတ်

Array/String မှာ အသုံးများတဲ့ operation တွေနဲ့ သူတို့ရဲ့ time complexity-

Operation ရှင်းလင်းချက် Time Complexity
Access arr[i] index နဲ့ ဖတ်ခြင်း O(1)O(1)
Update arr[i] = x index နဲ့ ပြင်ခြင်း O(1)O(1)
Append (Dynamic Array) အဆုံးမှာ ထည့်ခြင်း O(1)O(1) amortized
Insert/Delete (အလယ်) element ရွှေ့ရခြင်း O(n)O(n)
Slice (subarray/substring) အပိုင်းတစ်ခု ဖြတ်ယူ၍ အသစ်ကူးခြင်း O(k)O(k) (kk = ဖြတ်ယူသော အရွယ်)
Search value တစ်ခု ရှာခြင်း O(n)O(n)

Append vs Slice မှတ်ချက်: Append (O(1)O(1) amortized) က နေရာရှိရင် တန်းထည့်ရုံပါပဲ။ Slice ကတော့ ဖြတ်ယူတဲ့ element အရေအတွက်အလိုက် အသစ်ကူးရတာကြောင့် O(k)O(k) ကုန်ပါတယ်။

Real-world Examples

Array နဲ့ String ကို နေ့စဉ် software development မှာ နေရာတိုင်းနီးပါး တွေ့ရပါတယ်-

Common Problems

ဒီအခန်းက concept တွေကို လေ့ကျင့်ဖို့ classic problem အချို့-

၁။ Max/Min ရှာခြင်း

int[] nums = {3, 7, 1, 9, 4};
int max = nums[0];
int min = nums[0];
for (int i = 1; i < nums.length; i++) {
    if (nums[i] > max) max = nums[i];
    if (nums[i] < min) min = nums[i];
}
System.out.println(max); // 9
System.out.println(min); // 1

Array တစ်ခေါက် (one pass) လှည့်ရုံနဲ့ max နဲ့ min နှစ်ခုလုံး ရတဲ့အတွက် O(n)O(n) ဖြစ်ပါတယ်။

၂။ String Reverse လုပ်ခြင်း

String s = "HELLO";
StringBuilder sb = new StringBuilder(s);
System.out.println(sb.reverse().toString()); // OLLEH

String က immutable ဖြစ်လို့ StringBuilder သုံးတာ ပိုသင့်ပါတယ်။ O(n)O(n) ဖြစ်ပါတယ်။

၃။ Character ရေတွက်ခြင်း (Count characters)

String s = "banana";
int[] count = new int[26]; // a-z
for (char c : s.toCharArray()) {
    count[c - 'a']++;
}
System.out.println(count['a' - 'a']); // 'a' ၃ လုံး

စာလုံးတစ်လုံးချင်း လှည့်ရတဲ့အတွက် O(n)O(n) ဖြစ်ပါတယ်။

၄။ Duplicate များ ဖယ်ရှားခြင်း (Remove duplicates)

int[] nums = {1, 2, 2, 3, 3, 3};
java.util.LinkedHashSet<Integer> set = new java.util.LinkedHashSet<>();
for (int n : nums) set.add(n);
System.out.println(set); // [1, 2, 3]

Set ကို သုံးပြီး ထပ်နေတဲ့ တန်ဖိုးတွေကို ဖယ်လိုက်တာ ဖြစ်ပါတယ်။ (Set အကြောင်းကို နောက်အခန်းမှာ ဆက်ဖတ်ပါ။)