دانستنی‌ها

ده چیزی که در مورد جاوا نمی‌دانستید!

شما از کسانی هستید که از زمانی که  شئ‌گرایی رونق داشت جاوا را می‌شناختید، از زمانی که به نام Oak صدا زده می‌شد و از هنگامی که طرفداران c++i هیچ شانسی برای پیشرفت جاوا قائل نمی‌شدند با آن کار می‌کردید؟
حتی اگر شما از ابتدا با جاوا آشنا بودید و با آن کار می‌کردید، قول می‌دهم که بیش از نیمی از موارد زیر را نمی‌دانید.
پس بیایید با ۱۰ شگفتی از عملکرد داخلی جاوا آشنا شویم.

 ۱-چیزی به نام checked Exception وجود ندارد
درست است! JVM اصلا چنین چیزی نمی‌شناسد و فقط زبان جاوا آن را می‌داند.
امروز همه موافق هستند که checked Exceptionها اشتباه بودند. هیچ زبان دیگری بعد از جاوا از chekced Exception استفاده نکرده است و حتی در جاوا ۸ در APIهای استریم‌ها وجود ندارند.
برای اثبات عدم شناخت JVM از این خطاها کد زیر را امتحان کنید:

public class Test {
 
    // No throws clause here
    public static void main(String[] args) {
        doThrow(new SQLException());
    }
 
    static void doThrow(Exception e) {
        Test.<RuntimeException> doThrow0(e);
    }
 
    @SuppressWarnings("unchecked")
    static <E extends Exception> 
    void doThrow0(Exception e) throws E {
        throw (E) e;
    }
}

نه تنها این کد کامپایل می‌شود بلکه خطای SQLException هم پرتاب می‌کند بدون آنکه نیاز به بلوک try/catch یا جمله throws Exception در امضای تابع باشد!

۲- می‌توان تابع‌هایی که تنها در نوع خروجی متفاوت هستند را overload کرد.

کامپایل نمی‌شود درسته؟

class Test {
    Object x() { return "abc"; }
    String x() { return "123"; }
}

زبان جاوا اجازه نمی‌دهد که دو تابع در یک کلاس با نام یکسان بدون تفاوت در نوع خروجی یا عبارت throws تعریف شوند.

اگر جاواداک مربوط به کلاس Class.getMethod(String, class…)i را چک کنیم نوشته شده:
“توجه کنید که ممکن است بیش از یک متد در یک کلاس مطابقت داشته باشد چرا که جاوا اجازه تعریف توابع با امضای یکسان و نوع خروجی متفاوت را به یک کلاس نمی‌دهد اما JVM این‌گونه نیست. این مساله انعطاف پذیری را در ماشین مجازی افزایش می‌دهد که بتواند قابلیت‌های مختلف زبان را پیاده‌سازی کند. برای مثال covariant returns می‌توانند با توابع bridge پیاده شوند. توابع bridge و توابع override شده یک امضا ولی خروجی‌های مختلف خواهند داشت.”
پس اگر کد زیر را بنویسیم داریم:

abstract class Parent<T> {
    abstract T x();
}

class Child extends Parent<String> {
    @Override
    String x() { return "abc"; }
}

کد تولید شده در بایت کد کلاس Child را بررسی می‌کنیم.

// Method descriptor #15 ()Ljava/lang/String;
  // Stack: 1, Locals: 1
  java.lang.String x();
    0  ldc <String "abc"> [16]
    2  areturn
      Line numbers:
        [pc: 0, line: 7]
      Local variable table:
        [pc: 0, pc: 3] local: this index: 0 type: Child
  
  // Method descriptor #18 ()Ljava/lang/Object;
  // Stack: 1, Locals: 1
  bridge synthetic java.lang.Object x();
    0  aload_0 [this]
    1  invokevirtual Child.x() : java.lang.String [19]
    4  areturn
      Line numbers:
        [pc: 0, line: 1]

بنابراین T تنها یک Object در بایت کد است. تابع bridge ساخته‌شده توسط کامپایلر تولید می‌شود چرا که نوع خروجی در امضای تابع Parent.x() ممکن است انتظار برود در بعضی از نقاط فراخوانی Object باشد. اضافه کردن generic بدون چنین توابع bridgeای برای binary comaptible بودن امکان پذیر نمی‌شد. بنابراین تغییر JVM برای اجازه دادن به چنین قابلیت‌هایی راحت‌تر بود. راه‌حل هوشمندانه‌ای بود، نه؟!

۳- تمام این‌ها آرایه‌های دوبعدی هستند!

class Test {
    int[][] a()  { return new int[0][]; }
    int[] b() [] { return new int[0][]; }
    int c() [][] { return new int[0][]; }
}

بله درست است، حتی اگر ذهن شما نتواند سریع پردازش کند که تمام عبارات فوق یکسان هستند. مشابه کد زیر:

class Test {
    int[][] a = {{}};
    int[] b[] = {{}};
    int c[][] = {{}};
}

فکر می‌کنید احمقانه است؟ حال تصور کنید ویژگی type annotation در جاوا ۸ را به همراه آن به کار ببریم. تعداد حالات بی‌شمار خواهد شد!

@Target(ElementType.TYPE_USE)
@interface Crazy {}

class Test {
    @Crazy int[][]  a1 = {{}};
    int @Crazy [][] a2 = {{}};
    int[] @Crazy [] a3 = {{}};

    @Crazy int[] b1[]  = {{}};
    int @Crazy [] b2[] = {{}};
    int[] b3 @Crazy [] = {{}};

    @Crazy int c1[][]  = {{}};
    int c2 @Crazy [][] = {{}};
    int c3[] @Crazy [] = {{}};
}

۴- شما عبارات شرطی را درک نمی‌کنید
فکر می‌کنید که وقتی از عبارات شرطی استفاده می‌کنید همه چیز را درمورد آن می‌دانید؟ خب لازم است که بگوییم این طور نیست! اغلب شما فکر می‌کنید که کد

Object o1 = true ? new Integer(1) : new Double(2.0);

و کد زیر

Object o2;

if (true)
    o2 = new Integer(1);
else
    o2 = new Double(2.0);

یکسان هستند…خیر! بگذارید تست کنیم:

System.out.println(o1);
System.out.println(o2);

خروجی برنامه به شکل زیر خواهد بود:

1.0
1

عملگر شرط “هر وقت لازم بداند” نمایش نوع‌داده عددی را به کار می‌گیرد. شما از برنامه زیر انتظار خواهید داشت که nullPointerException پرتاب کند؟

Integer i = new Integer(1);
if (i.equals(1))
    i = null;
Double d = new Double(2.0);
Object o = true ? i : d; // NullPointerException!
System.out.println(o);

۵- شما حتی از عملگر انتساب ترکیبی هم چیزی نمی‌دانید!
دو خط کد زیر را در نظر بگیرید:

i += j;
i = i + j;

ظاهرا باید معادل باشند، درسته؟ اما نیستند! در JLS آمده است.

عملگر انتساب مرکب به فرم E1 op= E2 معادل است با E1 = (T)((E1) op(E2))i که در آن T از نوع E1 است. به جز در مواردی که E1 تنها یک بار ارزیابی می‌شود.
این مساله خیلی زیباست مثال‌های زیر را ببینید:

byte b = 10;
b *= 5.7;
System.out.println(b); // prints 57

یا

byte b = 100;
b /= 2.5;
System.out.println(b); // prints 40

یا

char ch = '0';
ch *= 1.1;
System.out.println(ch); // prints '4'

یا

char ch = 'A';
ch *= 1.5;
System.out.println(ch); // prints 'a'

حالا واقعا چقدر مفید خواهد بود؟ بریم کاراکترهای توی کدمون را *= کنیم….!

۶- اعداد رندوم
حالا یک معمای بزرگ‌تر. به پاسخ نگاه نکنید ببینید می‌توانید خودتان کشف کنید. وقتی برنامه زیر را اجرا می‌کنیم:

for (int i = 0; i < 10; i++) {
  System.out.println((Integer) i);
}

“گاها” خروجی زیر را دریافت می‌کنیم:

92
221
45
48
236
183
39
193
33
84

چطور ممکن است؟
.
.
.
.
.
راه حل اینجاست (به Integer cache overriding در JDK با استفاده از reflection و سپس استفاده از auto-boxing , auto_unboxing برمی‌گردد)

۷- GOTO
این یکی از موارد مورد علاقه من است. جاوا GOTO دارد! این کد را بنویسید.

int goto = 1;

نتیجه زیر را در برخواهد داشت!

Test.java:44: error: <identifier> expected
    int goto = 1;
       ^

به این دلیل است که جاوا کلید واژه استفاده نشده GOTO دارد.

اما نکته جالب کار این است که شما خودتان می‌توانید goto را تعریف کنید
پرش به جلو:

label: {
  // do stuff
  if (check) break label;
  // do more stuff
}

در بایت کد

2  iload_1 [check]
3  ifeq 6          // Jumping forward
6  ..

پرش به عقب

label: do {
  // do stuff
  if (check) continue label;
  // do more stuff
  break label;
} while(true);

در بایت کد

2  iload_1 [check]
 3  ifeq 9
 6  goto 2          // Jumping backward
 9  ..

۸- جاوا برای نوع داده‌ها اسم مستعار دارد
در زبان‌های دیگر به سادگی می‌توان اسم مستعار تعریف کرد:

interface People => Set<Person>;

که از این به بعد به جای set به راحتی می‌توان از People استفاده کرد.

People?      p1 = null;
Set<Person>? p2 = p1;
People?      p3 = p2;

در جاوا در سطوح بالا نمی‌توانیم اسم مستعار برای نوع داده‌ها تعریف کنیم اما می‌توانیم این کار را در اسکوپ یک کلاس یا متد انجام دهیم. فرض کنید ما از نامگذاری‌های Integer, Long, … خوشمان نمی‌آید و می‌خواهیم اسامی کوتاه‌تری مثل I, L, … را داشته باشیم. ساده است:

class Test<I extends Integer> {
    <L extends Long> void x(I i, L l) {
        System.out.println(
            i.intValue() + ", " + 
            l.longValue()
        );
    }
}

در اسکوپ کلاس Test بالا، I اسم مستعاری برای Integer و در اسکوپ تابع x این کلاس L اسم مستعاری برای Long شده است. پس می‌توان متد بالا را به شکل زیر فراخوانی کرد:

new Test().x(1, 2L);

این تکنیک‌ها البته جدی نیستند. در این جا Integer, Long هر دو نوع داده‌های نهایی هستند و این یعنی I, L واقعا اسم مستعارند اما اگر این طور نبود و مثلا با نوع داده Object سروکار داشتیم آن وقت از یک generic ساده استفاده کرده بودیم.

۹- بعضی از روابط بین نوع‌داده‌ها مشخص نیست!
این مورد می‌تواند اندکی ترسناک باشد پس یک لیوان چایی بریزید و تمرکز کنید. این دو نوع را در نظر بگیرید:

// A helper type. You could also just use List
interface Type<T> {}

class C implements Type<Type<? super C>> {}
class D<P> implements Type<Type<? super D<D<P>>>> {}

نوع داده‌های C, D چه معنی می‌دهند؟
این‌ها تقریبا بازگشتی هستند مشابه شیوه‌ای که java.lang.Enum بازگشتی است در نظر بگیرید:

public abstract class Enum<E extends Enum<E>> { ... }

با تعریف فوق پیاده‌سازی enum تنها یک معادل نحوی برای ساده‌سازی خواهد بود:

// This
enum MyEnum {}

// Is really just sugar for this
class MyEnum extends Enum<MyEnum> { ... }

با در نظر داشتن این مساله به مثال خودمان برمی‌گردیم. آیا تکه کد زیر کامپایل خواهد شد؟

class Test {
    Type<? super C> c = new C();
    Type<? super D<Byte>> d = new D<Byte>();
}

سوال سختی است!
آیا C یک زیرنوع از Type<? super C>i است؟

Step 0) C <?: Type<? super C>
Step 1) Type<Type<? super C>> <?: Type (inheritance)
Step 2) C  (checking wildcard ? super C)
Step . . . (cycle forever)

یا آیا D یک زیر نوع از Type<? super D<Byte>>i است؟

Step 0) D<Byte> <?: Type<? super C<Byte>>
Step 1) Type<Type<? super D<D<Byte>>>> <?: Type<? super D<Byte>>
Step 2) D<Byte> <?: Type<? super D<D<Byte>>>
Step 3) List<List<? super C<C>>> <?: List<? super C<C>>
Step 4) D<D<Byte>> <?: Type<? super D<D<Byte>>>
Step . . . (expand forever)

اگر کد بالا را در اکلیپس امتحان کرده باشید می‌بینید که کرش خواهد کرد! در کل می‌توان گفت که ارتباط بعضی از نوع‌داده‌ها قابل تصمیم‌گیری نیست.

۱۰- اشتراک نوع‌داده‌ها
یک ویژگی عجیبی که جاوا دارد اشتراک نوع‌داده‌هاست. می‌توان نوع‌داده‌ای تعریف کرد که در حقیقت اشتراک دو نوع داده دیگر است. برای مثال:

class Test<T extends Serializable & Cloneable> {
}

هر نوع‌داده‌ای که قرار است به جای T استفاده شود باید هر دو کلاس Serializable , Cloneable را پیاده‌سازی کرده باشد مثلا String اینطور نیست ولی Date می‌تواند باشد:

// Doesn't compile
Test<String> s = null;

// Compiles
Test<Date> d = null;

این ویژگی در جاوا ۸ هم استفاده شده که می‌توان نوع‌داده‌ها را به اشتراک نوع‌داده‌های خاص منظوره تبدیل کرد. اما این به چه دردی می‌خورد؟ حقیقتا این اصلا به درد بخور نیست! اما اگر بخواهیم یک lambda expression را به چنین نوع داده‌ای تبدیل کنیم راه دیگری نداریم. فرض کنید محدودیت‌های نوع‌داده مسخره زیر را روی تابع خود داشته باشید:

<T extends Runnable & Serializable> void execute(T t) {}

شما یک Runnableای می‌خواهید که Serializable هم باشد برای شرایطی که هم می‌خواهید آن را در جایی دیگر اجرا کنید و هم روی سیم بفرستید. lambda و Serialization اندکی مشابه اند!
شما می‌توانید یک lambda expression را serialize کنید اگر نوع‌داده مقصد و آرگومان‌های آن serializable بودند.
اما اگر این هم درست باشد به طور اتوماتیک serializable را پیاده‌سازی نمی‌کنند. برای تبدیل آن‌ها به آن نوع‌داده لازم است تبدیل صورت گیرد. اما وقتی فقط به Serializable تبدیل انجام شود.

execute((Serializable) (() -> {}));

آن‌گاه lambda دیگر runnable نخواهد بود. بنابراین باید تبدیل به هردو نوع‌داده صورت گیرد.

execute((Runnable & Serializable) (() -> {}));

جاوا زبان پر رمز و رازی است. شما چه عجایب دیگری از آن تا به حال کشف کرده‌اید؟

منابع:

javacodegeek
wikipedia
stackoverflow

نوشته های مشابه

یک دیدگاه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا