אנחנו רגילים לכתוב מספרים באמצעות עשר ספרות: `0` עד `9`. המחשב, לעומת זאת, בנוי מרכיבים אלקטרוניים שנוח להם להבחין בין שני מצבים: מופעל וכבוי, מתח גבוה ומתח נמוך, או פשוט `1` ו-`0`.
מכאן מגיע הבסיס הבינארי: שיטת ייצוג מספרים שמשתמשת בשתי ספרות בלבד.
אבל בינארי אינו רק דרך מוזרה לכתוב מספרים. הבנה שלו מסבירה כיצד מחשבים שומרים מידע, מנהלים הרשאות, מפעילים דגלים, מבצעים פעולות מהירות, מטפלים במספרים שליליים, ולפעמים גם מייצרים תוצאות מפתיעות בגלל overflow.
בסיס 10 לעומת בסיס 2
בשיטה העשרונית, ערכה של כל ספרה נקבע לפי המיקום שלה וחזקה של 10.
583 = 5*100 + 8*10 + 3*1
= 5*10^2 + 8*10^1 + 3*10^0בבינארי העיקרון זהה, אבל כל מיקום מבוסס על חזקה של 2:
... 32 16 8 4 2 1לדוגמה:
1011_2נמקם את הספרות מול ערכי העמודות:
ערך: 8 4 2 1
ביט: 1 0 1 1ולכן:
1011_2 = 8 + 0 + 2 + 1 = 11_10הספרה הימנית ביותר מייצגת תמיד את `2^0`, שערכו 1. משמאלה נמצאות `2^1`, `2^2`, `2^3` וכן הלאה.
המרה מעשרוני לבינארי
אחת השיטות הנוחות היא לפרק את המספר לחזקות של 2.
נמיר את 45:
45 = 32 + 8 + 4 + 1נכתוב את העמודות:
32 16 8 4 2 1ונסמן `1` במקום שבו השתמשנו בערך, ו-`0` במקום שלא:
32 16 8 4 2 1
1 0 1 1 0 1לכן:
45_10 = 101101_2דרך נוספת היא לחלק שוב ושוב ב-2 ולרשום את השאריות. לאחר מכן קוראים את השאריות מלמטה למעלה.
המרה מבינארי לעשרוני
ניקח את:
1010110_2נפרק לפי העמודות:
64 32 16 8 4 2 1
1 0 1 0 1 1 0ולכן:
64 + 16 + 4 + 2 = 86כלומר:
1010110_2 = 86_10חיבור וחיסור בינאריים
חיבור בינארי דומה לחיבור עשרוני, אך קיימות רק ארבע אפשרויות בסיסיות:
0 + 0 = 0
0 + 1 = 1
1 + 0 = 1
1 + 1 = 10התוצאה `10` פירושה שכותבים `0` בעמודה הנוכחית ומעבירים `1` לעמודה הבאה.
1011
+ 0101
------
10000בחיסור, כאשר צריך לחשב `0-1`, שואלים מהעמודה שמשמאל. הביט שנשאל הופך בעמודה הנוכחית ל-`10_2`, כלומר לשתי יחידות.
10101
- 00110
-------
01111בדיקה עשרונית:
21 - 6 = 15
01111_2 = 15חשוב לזכור: בזמן ההשאלה, הערכים הזמניים הם פירוק לפי עמודות. הם אינם בהכרח מספר בינארי רגיל שאפשר לקרוא ברצף.
הזזת ביטים ב-Java
ב-Java קיימים אופרטורים שמזיזים את כל הביטים ימינה או שמאלה.
הזזה שמאלה: `<<`
int result = 13 << 1;הייצוג של 13 הוא:
001101לאחר הזזה אחת שמאלה:
011010כלומר:
13 << 1 = 26באופן כללי, הזזה שמאלה ב-`n` מקומות שקולה בדרך כלל לכפל ב-`2^n`:
x << 1 ~= x * 2
x << 2 ~= x * 4
x << 3 ~= x * 8הסימן `~=` חשוב: השוויון תקף רק כאשר לא נאבדים ביטים ואין overflow שמשנה את התוצאה.
הזזה ימינה: `>>`
במספר חיובי, הזזה ימינה שקולה בדרך כלל לחלוקה שלמה בחזקה של 2:
int result = 29 >> 1; // 14בבינארי:
11101 -> 01110הביט הימני שנזרק ייצג את השארית. במקרה הזה הוא היה `1`, ולכן:
29 / 2 = 14 ושארית 1פעולות לוגיות על ביטים
פעולות ביטים משוות שני מספרים עמודה מול עמודה.
נשתמש במספרים:
A = 10110
B = 01101AND: `&`
התוצאה היא `1` רק כאשר שני הביטים הם `1`.
10110
& 01101
-------
00100OR: `|`
התוצאה היא `1` כאשר לפחות אחד מהביטים הוא `1`.
10110
| 01101
-------
11111XOR: `^`
התוצאה היא `1` כאשר הביטים שונים.
10110
^ 01101
-------
11011ההבדל המרכזי בין OR ל-XOR מופיע כאשר שני הביטים הם `1`:
1 OR 1 = 1
1 XOR 1 = 0NOT: `~`
NOT הופך כל ביט:
0 -> 1
1 -> 0ברוחב תאורטי של חמישה ביטים:
~10110 = 01001אבל `int` ב-Java מכיל 32 ביטים, ולכן `~` הופך את כל 32 הביטים, גם את אלה שאינם מוצגים בדוגמה הקצרה.
שימוש מעשי: שמירת הרשאות בתוך מספר אחד
נניח שלמערכת יש ארבע הרשאות:
private static final int READ = 1; // 0001
private static final int WRITE = 1 << 1; // 0010
private static final int EXECUTE = 1 << 2; // 0100
private static final int ADMIN = 1 << 3; // 1000המספר:
0101מייצג שתי הרשאות פעילות:
READ = 1
WRITE = 0
EXECUTE = 1
ADMIN = 0כלומר, למשתמש יש הרשאות קריאה והרצה.
הפעלת הרשאה עם OR
permissions |= WRITE;0101 | 0010 = 0111בדיקת הרשאה עם AND
boolean canExecute = (permissions & EXECUTE) != 0;אם תוצאת ה-AND אינה אפס, הביט קיים.
כיבוי הרשאה עם AND ו-NOT
permissions &= ~READ;פעולת `~READ` יוצרת מסכה שבה הביט של READ הוא `0` ושאר הביטים הם `1`. פעולת AND מכבה רק את READ.
החלפת מצב עם XOR
permissions ^= EXECUTE;אם EXECUTE היה פעיל, הוא יכבה. אם היה כבוי, הוא יופעל.
בדיקת הרשאה אחת, לפחות אחת או את כולן
נניח שהמסכה מכילה שתי הרשאות:
int mask = READ | WRITE;כדי לבדוק אם קיימת לפחות הרשאה אחת:
boolean hasAny = (permissions & mask) != 0;כדי לבדוק אם קיימות כל ההרשאות שבמסכה:
boolean hasAll = (permissions & mask) == mask;אלה שתי שאלות שונות.
permissions = 0101
mask = 0011תוצאת AND:
0101 & 0011 = 0001לכן:
hasAny = true
hasAll = falseיש הרשאה משותפת אחת, READ, אבל WRITE חסרה.
רוחב קבוע ו-overflow
במתמטיקה אפשר להוסיף ספרות לפי הצורך. במחשב, לכל טיפוס יש מספר קבוע של ביטים.
נניח שמותר לשמור ארבעה ביטים בלבד:
1101_2 = 13הזזה שמאלה נותנת תוצאה מלאה:
1101 << 1 = 11010_2 = 26אבל `11010` דורש חמישה ביטים. אם מותר לשמור רק ארבעה, הביט השמאלי נזרק:
11010
↓↓↓↓
1010התוצאה שנשמרת היא:
1010_2 = 10הביט שנמחק ייצג את הערך 16:
26 - 16 = 10זהו overflow: הפעולה התבצעה, אבל לא היה מספיק מקום לייצג את התוצאה המלאה.
ב-Java, `int` מכיל 32 ביטים. כאשר תוצאה עוברת את הטווח שלו, הביטים העודפים נזרקים והתוצאה עלולה לשנות סימן בלי שתיזרק שגיאה.
מספרים שליליים: ההבדל בין `>>` ל-`>>>`
Java מספקת שני אופרטורים להזזה ימינה.
הזזה אריתמטית: `>>`
האופרטור `>>` משכפל משמאל את ביט הסימן.
במספר שלילי, ביט הסימן הוא `1`, ולכן המספר נשאר שלילי:
-8 >> 1 == -4הזזה לוגית: `>>>`
האופרטור `>>>` מכניס תמיד `0` משמאל, גם כאשר המספר שלילי.
-8 >>> 1 == 2147483644התוצאה גדולה וחיובית משום שהביט השמאלי הפך ל-`0`, ושאר הביטים נשארו פעילים.
-16 >> 2 == -4
-16 >>> 2 == 1073741820מדוע `/` ו-`>>` שונים במספר שלילי אי-זוגי?
נבחן:
-11 / 2
-11 >> 1התוצאה המתמטית היא:
-11 / 2 = -5.5חלוקת מספרים שלמים באמצעות `/` ב-Java נחתכת לכיוון אפס:
-11 / 2 == -5לעומת זאת, הזזה אריתמטית ימינה מתנהגת במקרה הזה כמו עיגול כלפי מטה:
-11 >> 1 == -6לכן אסור להחליף אוטומטית חלוקה ב-2 בהזזה ימינה כאשר הערך עלול להיות שלילי.
חשוב גם לזכור: `//` אינו אופרטור חילוק ב-Java. הוא פותח הערה עד סוף השורה.
int result = -11 / 2; // התוצאה היא -5ניסוי מלא ב-Java
public class BinaryExperiment {
private static final int READ = 1;
private static final int WRITE = 1 << 1;
private static final int EXECUTE = 1 << 2;
private static final int ADMIN = 1 << 3;
public static void main(String[] args) {
int number = 45;
System.out.println(
"45 in binary: " + Integer.toBinaryString(number)
);
int parsed = Integer.parseInt("101101", 2);
System.out.println("101101 in decimal: " + parsed);
System.out.println("13 << 2: " + (13 << 2));
System.out.println("29 >> 1: " + (29 >> 1));
int permissions = READ | EXECUTE;
printPermissions("Initial", permissions);
permissions |= WRITE;
printPermissions("Enable WRITE", permissions);
permissions ^= EXECUTE;
printPermissions("Toggle EXECUTE", permissions);
permissions &= ~READ;
printPermissions("Disable READ", permissions);
permissions |= ADMIN;
printPermissions("Enable ADMIN", permissions);
int mask = WRITE | ADMIN;
boolean hasAny = (permissions & mask) != 0;
boolean hasAll = (permissions & mask) == mask;
System.out.println("Has any required permission: " + hasAny);
System.out.println("Has all required permissions: " + hasAll);
System.out.println("-11 / 2: " + (-11 / 2));
System.out.println("-11 >> 1: " + (-11 >> 1));
System.out.println("-16 >>> 2: " + (-16 >>> 2));
}
private static void printPermissions(String title, int value) {
String binary = String.format(
"%4s",
Integer.toBinaryString(value)
).replace(' ', '0');
System.out.println(title + ": " + binary + " (" + value + ")");
}
}טעויות נפוצות
הטעות הראשונה היא לקרוא מספר בינארי כאילו היה מספר עשרוני. `1011_2` אינו אלף ואחת עשרה, אלא 11.
טעות נוספת היא לחשוב שהזזה שמאלה תמיד שווה לכפל ושימינה תמיד שווה לחלוקה. הדבר נכון רק כל עוד לא נאבדים ביטים, אין overflow ומתחשבים נכון במספרים שליליים.
במסכות ביטים, הבדיקה:
(permissions & mask) != 0בודקת אם קיים לפחות ביט משותף אחד. היא אינה בודקת שכל הביטים במסכה קיימים. לבדיקת כולם יש להשתמש ב:
(permissions & mask) == maskבנוסף, יש להבחין בין תוצאת פעולת AND, שהיא מספר, לבין תוצאת ההשוואה ל-0, שהיא `boolean`.
לבסוף, פעולת `~` ב-Java אינה הופכת רק את הביטים שאנחנו מציגים על המסך. היא הופכת את כל הביטים של הטיפוס.
סיכום
בינארי הוא אותה שיטה מיקומית שאנחנו מכירים מבסיס 10, רק עם חזקות של 2 ועם הספרות `0` ו-`1`.
הבנה של בינארי מאפשרת לקרוא ולבנות מסכות ביטים, לנהל הרשאות ודגלים, להבין הזזות, לזהות overflow ולהימנע מהנחות שגויות לגבי מספרים שליליים.
המטרה אינה לבצע כל חישוב בינארי בראש. המטרה היא להבין מה הביטים מייצגים, כיצד פעולה משנה אותם, ומה עלול לקרות כאשר רוחב המספר מוגבל.
ברגע שרואים מספר כמערכת של ביטים נפרדים, פעולות שנראו בעבר מסתוריות, כמו `&`, `|`, `^`, `~`, `<<`, `>>` ו-`>>>`, הופכות לכלים ברורים ושימושיים.