تم نشر هذه المقالة في الأصل على مدونتي.
يجب عليك أيضًا قراءة برنامج Java 11 التعليمي (بما في ذلك ميزات اللغة وواجهة برمجة التطبيقات الجديدة من Java 9 و10 و11).
مرحبًا بك في مقدمتي لـ Java 8. يرشدك هذا البرنامج التعليمي خطوة بخطوة عبر جميع ميزات اللغة الجديدة. ستتعلم، مدعومة بنماذج تعليمات برمجية قصيرة وبسيطة، كيفية استخدام أساليب الواجهة الافتراضية وتعبيرات لامدا ومراجع الأساليب والتعليقات التوضيحية القابلة للتكرار. في نهاية المقالة، ستكون على دراية بأحدث تغييرات واجهة برمجة التطبيقات مثل التدفقات والواجهات الوظيفية وامتدادات الخرائط وواجهة برمجة تطبيقات التاريخ الجديدة. لا توجد جدران نصية، فقط مجموعة من مقتطفات التعليمات البرمجية التي تم التعليق عليها. يتمتع!
★★★ هل أعجبك هذا المشروع؟ اترك نجمة أو تابع على تويتر أو تبرع لدعم عملي. شكرًا! ★★★
يمكّننا Java 8 من إضافة تطبيقات الطريقة غير المجردة إلى الواجهات من خلال استخدام الكلمة الأساسية default
. تُعرف هذه الميزة أيضًا بطرق الامتداد الافتراضية.
هنا هو المثال الأول لدينا:
interface Formula {
double calculate ( int a );
default double sqrt ( int a ) {
return Math . sqrt ( a );
}
}
إلى جانب الطريقة المجردة calculate
الواجهة، تحدد Formula
أيضًا الطريقة الافتراضية sqrt
. يجب على الفئات الملموسة فقط تنفيذ الطريقة المجردة calculate
. يمكن استخدام الطريقة الافتراضية sqrt
خارج الصندوق.
Formula formula = new Formula () {
@ Override
public double calculate ( int a ) {
return sqrt ( a * 100 );
}
};
formula . calculate ( 100 ); // 100.0
formula . sqrt ( 16 ); // 4.0
يتم تنفيذ الصيغة ككائن مجهول. الكود مطول تمامًا: 6 أسطر من الكود لمثل هذا الحساب البسيط لـ sqrt(a * 100)
. كما سنرى في القسم التالي، هناك طريقة أفضل بكثير لتنفيذ كائنات الطريقة الواحدة في Java 8.
لنبدأ بمثال بسيط لكيفية فرز قائمة السلاسل في الإصدارات السابقة من Java:
List < String > names = Arrays . asList ( "peter" , "anna" , "mike" , "xenia" );
Collections . sort ( names , new Comparator < String >() {
@ Override
public int compare ( String a , String b ) {
return b . compareTo ( a );
}
});
تقبل طريقة الأداة المساعدة الثابتة Collections.sort
قائمة ومقارنة لفرز عناصر القائمة المحددة. غالبًا ما تجد نفسك تقوم بإنشاء مقارنات مجهولة وتمريرها إلى طريقة الفرز.
بدلاً من إنشاء كائنات مجهولة طوال اليوم، يأتي Java 8 مع بناء جملة أقصر بكثير، تعبيرات لامدا :
Collections . sort ( names , ( String a , String b ) -> {
return b . compareTo ( a );
});
كما ترون فإن الكود أقصر بكثير وأسهل في القراءة. لكنها تصبح أقصر:
Collections . sort ( names , ( String a , String b ) -> b . compareTo ( a ));
بالنسبة لنصوص طريقة السطر الواحد، يمكنك تخطي كل من الأقواس المعقوفة {}
والكلمة الأساسية return
. لكنها تصبح أقصر:
names . sort (( a , b ) -> b . compareTo ( a ));
تحتوي القائمة الآن على طريقة sort
. كما أن مترجم جافا على علم بأنواع المعلمات حتى تتمكن من تخطيها أيضًا. دعونا نتعمق أكثر في كيفية استخدام تعبيرات لامدا في البرية.
كيف تتناسب تعبيرات لامدا مع نظام الكتابة في Java؟ يتوافق كل لامدا مع نوع معين، تحدده الواجهة. يجب أن تحتوي الواجهة الوظيفية المزعومة على إعلان طريقة مجردة واحد بالضبط . ستتم مطابقة كل تعبير لامدا من هذا النوع بهذه الطريقة المجردة. نظرًا لأن الطرق الافتراضية ليست مجردة، فلديك الحرية في إضافة طرق افتراضية إلى الواجهة الوظيفية الخاصة بك.
يمكننا استخدام واجهات عشوائية كتعبيرات لامدا طالما أن الواجهة تحتوي على طريقة مجردة واحدة فقط. للتأكد من أن واجهتك تلبي المتطلبات، يجب عليك إضافة التعليق التوضيحي @FunctionalInterface
. المترجم على علم بهذا التعليق التوضيحي ويلقي خطأ في المترجم بمجرد محاولة إضافة إعلان أسلوب مجردة ثانٍ إلى الواجهة.
مثال:
@ FunctionalInterface
interface Converter < F , T > {
T convert ( F from );
}
Converter < String , Integer > converter = ( from ) -> Integer . valueOf ( from );
Integer converted = converter . convert ( "123" );
System . out . println ( converted ); // 123
ضع في اعتبارك أن الكود صالح أيضًا إذا تم حذف التعليق التوضيحي @FunctionalInterface
.
يمكن تبسيط رمز المثال أعلاه بشكل أكبر من خلال استخدام مراجع الطريقة الثابتة:
Converter < String , Integer > converter = Integer :: valueOf ;
Integer converted = converter . convert ( "123" );
System . out . println ( converted ); // 123
يمكّنك Java 8 من تمرير مراجع الأساليب أو المنشئات عبر الكلمة الأساسية ::
. يوضح المثال أعلاه كيفية الرجوع إلى طريقة ثابتة. ولكن يمكننا أيضًا الرجوع إلى أساليب الكائن:
class Something {
String startsWith ( String s ) {
return String . valueOf ( s . charAt ( 0 ));
}
}
Something something = new Something ();
Converter < String , String > converter = something :: startsWith ;
String converted = converter . convert ( "Java" );
System . out . println ( converted ); // "J"
دعونا نرى كيف تعمل الكلمة الأساسية ::
للمنشئين. أولاً نحدد فئة مثال بمنشئات مختلفة:
class Person {
String firstName ;
String lastName ;
Person () {}
Person ( String firstName , String lastName ) {
this . firstName = firstName ;
this . lastName = lastName ;
}
}
بعد ذلك نحدد واجهة مصنع الشخص لاستخدامها في إنشاء أشخاص جدد:
interface PersonFactory < P extends Person > {
P create ( String firstName , String lastName );
}
بدلًا من تنفيذ المصنع يدويًا، نلصق كل شيء معًا عبر مراجع المنشئ:
PersonFactory < Person > personFactory = Person :: new ;
Person person = personFactory . create ( "Peter" , "Parker" );
نقوم بإنشاء مرجع إلى مُنشئ الشخص عبر Person::new
. يقوم مترجم Java تلقائيًا باختيار المُنشئ الصحيح عن طريق مطابقة توقيع PersonFactory.create
.
الوصول إلى متغيرات النطاق الخارجي من تعبيرات لامدا يشبه إلى حد كبير الكائنات المجهولة. يمكنك الوصول إلى المتغيرات النهائية من النطاق الخارجي المحلي بالإضافة إلى حقول المثيلات والمتغيرات الثابتة.
يمكننا قراءة المتغيرات المحلية النهائية من النطاق الخارجي لتعبيرات لامدا:
final int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
stringConverter . convert ( 2 ); // 3
لكن بخلاف الكائنات المجهولة، لا يلزم إعلان المتغير num
نهائيًا. هذا الرمز صالح أيضًا:
int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
stringConverter . convert ( 2 ); // 3
ومع ذلك، يجب أن يكون num
نهائيًا ضمنيًا حتى يتم تجميع التعليمات البرمجية. لا يتم تجميع التعليمات البرمجية التالية:
int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
num = 3 ;
الكتابة إلى num
من داخل تعبير lambda محظورة أيضًا.
على النقيض من المتغيرات المحلية، لدينا إمكانية الوصول للقراءة والكتابة إلى حقول المثيلات والمتغيرات الثابتة من داخل تعبيرات لامدا. هذا السلوك معروف جيدًا من الكائنات المجهولة.
class Lambda4 {
static int outerStaticNum ;
int outerNum ;
void testScopes () {
Converter < Integer , String > stringConverter1 = ( from ) -> {
outerNum = 23 ;
return String . valueOf ( from );
};
Converter < Integer , String > stringConverter2 = ( from ) -> {
outerStaticNum = 72 ;
return String . valueOf ( from );
};
}
}
هل تتذكر مثال الصيغة من القسم الأول؟ تحدد Formula
الواجهة الطريقة الافتراضية sqrt
التي يمكن الوصول إليها من كل نسخة صيغة بما في ذلك الكائنات المجهولة. هذا لا يعمل مع تعبيرات لامدا.
لا يمكن الوصول إلى الأساليب الافتراضية من داخل تعبيرات لامدا. لا يتم تجميع التعليمات البرمجية التالية:
Formula formula = ( a ) -> sqrt ( a * 100 );
تحتوي JDK 1.8 API على العديد من الواجهات الوظيفية المضمنة. بعضها معروف جيدًا من الإصدارات القديمة من Java مثل Comparator
أو Runnable
. يتم توسيع هذه الواجهات الحالية لتمكين دعم Lambda عبر التعليق التوضيحي @FunctionalInterface
.
لكن Java 8 API مليئة أيضًا بالواجهات الوظيفية الجديدة لتسهيل حياتك. بعض هذه الواجهات الجديدة معروفة جيدًا من مكتبة Google Guava. حتى لو كنت على دراية بهذه المكتبة، فيجب عليك أن تراقب عن كثب كيفية توسيع هذه الواجهات بواسطة بعض ملحقات الطرق المفيدة.
المسندات هي وظائف ذات قيمة منطقية لوسيطة واحدة. تحتوي الواجهة على طرق افتراضية متنوعة لتكوين المسندات للمصطلحات المنطقية المعقدة (و، أو، نفي)
Predicate < String > predicate = ( s ) -> s . length () > 0 ;
predicate . test ( "foo" ); // true
predicate . negate (). test ( "foo" ); // false
Predicate < Boolean > nonNull = Objects :: nonNull ;
Predicate < Boolean > isNull = Objects :: isNull ;
Predicate < String > isEmpty = String :: isEmpty ;
Predicate < String > isNotEmpty = isEmpty . negate ();
تقبل الوظائف وسيطة واحدة وتنتج نتيجة. يمكن استخدام الطرق الافتراضية لربط وظائف متعددة معًا (إنشاء، ثم).
Function < String , Integer > toInteger = Integer :: valueOf ;
Function < String , String > backToString = toInteger . andThen ( String :: valueOf );
backToString . apply ( "123" ); // "123"
يقوم الموردون بإنتاج نتيجة لنوع عام معين. على عكس الوظائف، لا يقبل الموردون الوسائط.
Supplier < Person > personSupplier = Person :: new ;
personSupplier . get (); // new Person
يمثل المستهلكون العمليات التي سيتم تنفيذها على وسيطة إدخال واحدة.
Consumer < Person > greeter = ( p ) -> System . out . println ( "Hello, " + p . firstName );
greeter . accept ( new Person ( "Luke" , "Skywalker" ));
المقارنات معروفة جيدًا في الإصدارات الأقدم من Java. يضيف Java 8 أساليب افتراضية متنوعة إلى الواجهة.
Comparator < Person > comparator = ( p1 , p2 ) -> p1 . firstName . compareTo ( p2 . firstName );
Person p1 = new Person ( "John" , "Doe" );
Person p2 = new Person ( "Alice" , "Wonderland" );
comparator . compare ( p1 , p2 ); // > 0
comparator . reversed (). compare ( p1 , p2 ); // < 0
الخيارات الاختيارية ليست واجهات وظيفية، ولكنها أدوات مساعدة أنيقة لمنع NullPointerException
. إنه مفهوم مهم للقسم التالي، لذلك دعونا نلقي نظرة سريعة على كيفية عمل الاختيارات.
الاختيارية عبارة عن حاوية بسيطة لقيمة قد تكون فارغة أو غير فارغة. فكر في طريقة قد تُرجع نتيجة غير فارغة ولكن في بعض الأحيان لا تُرجع شيئًا. بدلاً من إرجاع null
، يمكنك إرجاع Optional
في Java 8.
Optional < String > optional = Optional . of ( "bam" );
optional . isPresent (); // true
optional . get (); // "bam"
optional . orElse ( "fallback" ); // "bam"
optional . ifPresent (( s ) -> System . out . println ( s . charAt ( 0 ))); // "b"
يمثل java.util.Stream
سلسلة من العناصر التي يمكن إجراء عملية واحدة أو أكثر عليها. تكون عمليات الدفق إما وسيطة أو طرفية . بينما تقوم العمليات الطرفية بإرجاع نتيجة من نوع معين، تقوم العمليات الوسيطة بإرجاع الدفق نفسه حتى تتمكن من إجراء سلسلة من استدعاءات الأساليب المتعددة على التوالي. يتم إنشاء التدفقات على مصدر، على سبيل المثال، java.util.Collection
مثل القوائم أو المجموعات (الخرائط غير مدعومة). يمكن تنفيذ عمليات الدفق بشكل تسلسلي أو متوازي.
تعد التدفقات قوية للغاية، لذلك كتبت برنامجًا تعليميًا منفصلاً لـ Java 8 Streams. يجب عليك أيضًا التحقق من Sequency كمكتبة مماثلة للويب.
دعونا نلقي نظرة أولاً على كيفية عمل التدفقات المتسلسلة. أولاً نقوم بإنشاء مصدر عينة في شكل قائمة من السلاسل:
List < String > stringCollection = new ArrayList <>();
stringCollection . add ( "ddd2" );
stringCollection . add ( "aaa2" );
stringCollection . add ( "bbb1" );
stringCollection . add ( "aaa1" );
stringCollection . add ( "bbb3" );
stringCollection . add ( "ccc" );
stringCollection . add ( "bbb2" );
stringCollection . add ( "ddd1" );
يتم توسيع المجموعات في Java 8 بحيث يمكنك ببساطة إنشاء التدفقات إما عن طريق استدعاء Collection.stream()
أو Collection.parallelStream()
. تشرح الأقسام التالية عمليات الدفق الأكثر شيوعًا.
يقبل عامل التصفية المسند لتصفية جميع عناصر الدفق. هذه العملية متوسطة والتي تمكننا من استدعاء عملية دفق أخرى ( forEach
) على النتيجة. يقبل ForEach تنفيذ المستهلك لكل عنصر في الدفق الذي تمت تصفيته. ForEach هي عملية طرفية. إنه void
، لذلك لا يمكننا استدعاء عملية دفق أخرى.
stringCollection
. stream ()
. filter (( s ) -> s . startsWith ( "a" ))
. forEach ( System . out :: println );
// "aaa2", "aaa1"
Sorted هي عملية وسيطة تقوم بإرجاع طريقة عرض مرتبة للدفق. يتم فرز العناصر بترتيب طبيعي إلا إذا قمت بتمرير Comparator
مخصص.
stringCollection
. stream ()
. sorted ()
. filter (( s ) -> s . startsWith ( "a" ))
. forEach ( System . out :: println );
// "aaa1", "aaa2"
ضع في اعتبارك أن sorted
لا يؤدي إلا إلى إنشاء عرض مفروز للدفق دون التلاعب بترتيب المجموعة المدعومة. ترتيب stringCollection
لم يمس:
System . out . println ( stringCollection );
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
تقوم map
العمليات الوسيطة بتحويل كل عنصر إلى كائن آخر عبر الوظيفة المحددة. يقوم المثال التالي بتحويل كل سلسلة إلى سلسلة بأحرف كبيرة. ولكن يمكنك أيضًا استخدام map
لتحويل كل كائن إلى نوع آخر. يعتمد النوع العام للتدفق الناتج على النوع العام للوظيفة التي تمررها إلى map
.
stringCollection
. stream ()
. map ( String :: toUpperCase )
. sorted (( a , b ) -> b . compareTo ( a ))
. forEach ( System . out :: println );
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
يمكن استخدام عمليات مطابقة مختلفة للتحقق مما إذا كان مسند معين يطابق الدفق. كل هذه العمليات نهائية وترجع نتيجة منطقية.
boolean anyStartsWithA =
stringCollection
. stream ()
. anyMatch (( s ) -> s . startsWith ( "a" ));
System . out . println ( anyStartsWithA ); // true
boolean allStartsWithA =
stringCollection
. stream ()
. allMatch (( s ) -> s . startsWith ( "a" ));
System . out . println ( allStartsWithA ); // false
boolean noneStartsWithZ =
stringCollection
. stream ()
. noneMatch (( s ) -> s . startsWith ( "z" ));
System . out . println ( noneStartsWithZ ); // true
العد هو عملية طرفية تُرجع عدد العناصر في الدفق كقيمة long
.
long startsWithB =
stringCollection
. stream ()
. filter (( s ) -> s . startsWith ( "b" ))
. count ();
System . out . println ( startsWithB ); // 3
تؤدي هذه العملية الطرفية إلى تقليل عناصر الدفق باستخدام الوظيفة المحددة. والنتيجة هي Optional
يحتفظ بالقيمة المخفضة.
Optional < String > reduced =
stringCollection
. stream ()
. sorted ()
. reduce (( s1 , s2 ) -> s1 + "#" + s2 );
reduced . ifPresent ( System . out :: println );
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
كما ذكر أعلاه، يمكن أن تكون التدفقات متتابعة أو متوازية. يتم تنفيذ العمليات على التدفقات المتسلسلة على مؤشر ترابط واحد بينما يتم تنفيذ العمليات على التدفقات المتوازية بشكل متزامن على مؤشرات ترابط متعددة.
يوضح المثال التالي مدى سهولة زيادة الأداء باستخدام التدفقات المتوازية.
أولاً نقوم بإنشاء قائمة كبيرة من العناصر الفريدة:
int max = 1000000 ;
List < String > values = new ArrayList <>( max );
for ( int i = 0 ; i < max ; i ++) {
UUID uuid = UUID . randomUUID ();
values . add ( uuid . toString ());
}
الآن نقيس الوقت الذي يستغرقه فرز دفق هذه المجموعة.
long t0 = System . nanoTime ();
long count = values . stream (). sorted (). count ();
System . out . println ( count );
long t1 = System . nanoTime ();
long millis = TimeUnit . NANOSECONDS . toMillis ( t1 - t0 );
System . out . println ( String . format ( "sequential sort took: %d ms" , millis ));
// sequential sort took: 899 ms
long t0 = System . nanoTime ();
long count = values . parallelStream (). sorted (). count ();
System . out . println ( count );
long t1 = System . nanoTime ();
long millis = TimeUnit . NANOSECONDS . toMillis ( t1 - t0 );
System . out . println ( String . format ( "parallel sort took: %d ms" , millis ));
// parallel sort took: 472 ms
كما ترون، كلا مقتطفات التعليمات البرمجية متطابقة تقريبًا ولكن الفرز الموازي أسرع بنسبة 50٪ تقريبًا. كل ما عليك فعله هو تغيير stream()
إلى parallelStream()
.
كما ذكرنا سابقًا، لا تدعم الخرائط التدفقات بشكل مباشر. لا توجد طريقة stream()
متاحة على واجهة Map
نفسها، ومع ذلك يمكنك إنشاء تدفقات متخصصة بناءً على المفاتيح أو القيم أو إدخالات الخريطة عبر map.keySet().stream()
و map.values().stream()
و map.entrySet().stream()
.
علاوة على ذلك، تدعم الخرائط العديد من الأساليب الجديدة والمفيدة للقيام بالمهام المشتركة.
Map < Integer , String > map = new HashMap <>();
for ( int i = 0 ; i < 10 ; i ++) {
map . putIfAbsent ( i , "val" + i );
}
map . forEach (( id , val ) -> System . out . println ( val ));
يجب أن يكون الكود أعلاه واضحًا بذاته: يمنعنا putIfAbsent
من كتابة عمليات تحقق إضافية إذا كانت فارغة؛ يقبل forEach
المستهلك لإجراء عمليات لكل قيمة في الخريطة.
يوضح هذا المثال كيفية حساب التعليمات البرمجية على الخريطة باستخدام الوظائف:
map . computeIfPresent ( 3 , ( num , val ) -> val + num );
map . get ( 3 ); // val33
map . computeIfPresent ( 9 , ( num , val ) -> null );
map . containsKey ( 9 ); // false
map . computeIfAbsent ( 23 , num -> "val" + num );
map . containsKey ( 23 ); // true
map . computeIfAbsent ( 3 , num -> "bam" );
map . get ( 3 ); // val33
بعد ذلك، نتعلم كيفية إزالة الإدخالات لمفتاح معين، فقط إذا كان مرتبطًا حاليًا بقيمة معينة:
map . remove ( 3 , "val3" );
map . get ( 3 ); // val33
map . remove ( 3 , "val33" );
map . get ( 3 ); // null
طريقة أخرى مفيدة:
map . getOrDefault ( 42 , "not found" ); // not found
يعد دمج إدخالات الخريطة أمرًا سهلاً للغاية:
map . merge ( 9 , "val9" , ( value , newValue ) -> value . concat ( newValue ));
map . get ( 9 ); // val9
map . merge ( 9 , "concat" , ( value , newValue ) -> value . concat ( newValue ));
map . get ( 9 ); // val9concat
الدمج إما أن يضع المفتاح/القيمة في الخريطة في حالة عدم وجود إدخال للمفتاح، أو سيتم استدعاء وظيفة الدمج لتغيير القيمة الحالية.
يحتوي Java 8 على واجهة برمجة تطبيقات جديدة للتاريخ والوقت ضمن الحزمة java.time
. يمكن مقارنة واجهة برمجة التطبيقات Date API الجديدة بمكتبة Joda-Time، ولكنها ليست نفسها. تغطي الأمثلة التالية الأجزاء الأكثر أهمية في واجهة برمجة التطبيقات الجديدة هذه.
توفر الساعة إمكانية الوصول إلى التاريخ والوقت الحاليين. تدرك الساعات المنطقة الزمنية ويمكن استخدامها بدلاً من System.currentTimeMillis()
لاسترداد الوقت الحالي بالمللي ثانية منذ Unix EPOCH. يتم تمثيل هذه النقطة اللحظية على الخط الزمني أيضًا بواسطة الفئة Instant
. يمكن استخدام المراسلات الفورية لإنشاء كائنات java.util.Date
القديمة.
Clock clock = Clock . systemDefaultZone ();
long millis = clock . millis ();
Instant instant = clock . instant ();
Date legacyDate = Date . from ( instant ); // legacy java.util.Date
يتم تمثيل المناطق الزمنية بواسطة ZoneId
. يمكن الوصول إليها بسهولة عبر أساليب المصنع الثابتة. تحدد المناطق الزمنية الإزاحات المهمة للتحويل بين اللحظات والتواريخ والأوقات المحلية.
System . out . println ( ZoneId . getAvailableZoneIds ());
// prints all available timezone ids
ZoneId zone1 = ZoneId . of ( "Europe/Berlin" );
ZoneId zone2 = ZoneId . of ( "Brazil/East" );
System . out . println ( zone1 . getRules ());
System . out . println ( zone2 . getRules ());
// ZoneRules[currentStandardOffset=+01:00]
// ZoneRules[currentStandardOffset=-03:00]
يمثل LocalTime وقتًا بدون منطقة زمنية، على سبيل المثال 10 مساءً أو 17:30:15. يقوم المثال التالي بإنشاء توقيتين محليين للمناطق الزمنية المحددة أعلاه. ثم نقارن بين الوقتين ونحسب الفرق بالساعات والدقائق بين الوقتين.
LocalTime now1 = LocalTime . now ( zone1 );
LocalTime now2 = LocalTime . now ( zone2 );
System . out . println ( now1 . isBefore ( now2 )); // false
long hoursBetween = ChronoUnit . HOURS . between ( now1 , now2 );
long minutesBetween = ChronoUnit . MINUTES . between ( now1 , now2 );
System . out . println ( hoursBetween ); // -3
System . out . println ( minutesBetween ); // -239
يأتي LocalTime مزودًا بأساليب مصنعية متنوعة لتبسيط عملية إنشاء مثيلات جديدة، بما في ذلك تحليل السلاسل الزمنية.
LocalTime late = LocalTime . of ( 23 , 59 , 59 );
System . out . println ( late ); // 23:59:59
DateTimeFormatter germanFormatter =
DateTimeFormatter
. ofLocalizedTime ( FormatStyle . SHORT )
. withLocale ( Locale . GERMAN );
LocalTime leetTime = LocalTime . parse ( "13:37" , germanFormatter );
System . out . println ( leetTime ); // 13:37
يمثل LocalDate تاريخًا مميزًا، على سبيل المثال 2014-03-11. إنه غير قابل للتغيير ويعمل تمامًا مثل LocalTime. يوضح النموذج كيفية حساب التواريخ الجديدة عن طريق إضافة أو طرح أيام أو أشهر أو سنوات. ضع في اعتبارك أن كل عملية معالجة تُرجع مثيلًا جديدًا.
LocalDate today = LocalDate . now ();
LocalDate tomorrow = today . plus ( 1 , ChronoUnit . DAYS );
LocalDate yesterday = tomorrow . minusDays ( 2 );
LocalDate independenceDay = LocalDate . of ( 2014 , Month . JULY , 4 );
DayOfWeek dayOfWeek = independenceDay . getDayOfWeek ();
System . out . println ( dayOfWeek ); // FRIDAY
يعد تحليل LocalDate من سلسلة أمرًا بسيطًا تمامًا مثل تحليل LocalTime:
DateTimeFormatter germanFormatter =
DateTimeFormatter
. ofLocalizedDate ( FormatStyle . MEDIUM )
. withLocale ( Locale . GERMAN );
LocalDate xmas = LocalDate . parse ( "24.12.2014" , germanFormatter );
System . out . println ( xmas ); // 2014-12-24
LocalDateTime يمثل التاريخ والوقت. فهو يجمع بين التاريخ والوقت كما هو موضح في الأقسام أعلاه في حالة واحدة. LocalDateTime
غير قابل للتغيير ويعمل بشكل مشابه لـ LocalTime و LocalDate. يمكننا استخدام طرق لاسترداد حقول معينة من تاريخ ووقت:
LocalDateTime sylvester = LocalDateTime . of ( 2014 , Month . DECEMBER , 31 , 23 , 59 , 59 );
DayOfWeek dayOfWeek = sylvester . getDayOfWeek ();
System . out . println ( dayOfWeek ); // WEDNESDAY
Month month = sylvester . getMonth ();
System . out . println ( month ); // DECEMBER
long minuteOfDay = sylvester . getLong ( ChronoField . MINUTE_OF_DAY );
System . out . println ( minuteOfDay ); // 1439
ومن خلال المعلومات الإضافية الخاصة بالمنطقة الزمنية، يمكن تحويلها إلى لحظة. يمكن بسهولة تحويل التواريخ الفورية إلى تواريخ قديمة من النوع java.util.Date
.
Instant instant = sylvester
. atZone ( ZoneId . systemDefault ())
. toInstant ();
Date legacyDate = Date . from ( instant );
System . out . println ( legacyDate ); // Wed Dec 31 23:59:59 CET 2014
يعمل تنسيق أوقات التاريخ تمامًا مثل تنسيق التواريخ أو الأوقات. بدلاً من استخدام التنسيقات المحددة مسبقًا، يمكننا إنشاء تنسيقات من أنماط مخصصة.
DateTimeFormatter formatter =
DateTimeFormatter
. ofPattern ( "MMM dd, yyyy - HH:mm" );
LocalDateTime parsed = LocalDateTime . parse ( "Nov 03, 2014 - 07:13" , formatter );
String string = formatter . format ( parsed );
System . out . println ( string ); // Nov 03, 2014 - 07:13
على عكس java.text.NumberFormat
فإن DateTimeFormatter
الجديد غير قابل للتغيير وآمن لسلسلة الرسائل .
للحصول على تفاصيل حول بناء جملة النمط، اقرأ هنا.
التعليقات التوضيحية في Java 8 قابلة للتكرار. دعونا نتعمق مباشرة في مثال لمعرفة ذلك.
أولاً، نحدد تعليقًا توضيحيًا مجمّعًا يحتوي على مصفوفة من التعليقات التوضيحية الفعلية:
@interface Hints {
Hint [] value ();
}
@ Repeatable ( Hints . class )
@interface Hint {
String value ();
}
يمكّننا Java 8 من استخدام تعليقات توضيحية متعددة من نفس النوع من خلال الإعلان عن التعليق التوضيحي @Repeatable
.
@ Hints ({ @ Hint ( "hint1" ), @ Hint ( "hint2" )})
class Person {}
@ Hint ( "hint1" )
@ Hint ( "hint2" )
class Person {}
باستخدام المتغير 2، يقوم مترجم Java ضمنيًا بإعداد التعليق التوضيحي @Hints
أسفل الغطاء. وهذا أمر مهم لقراءة معلومات التعليقات التوضيحية من خلال التفكير.
Hint hint = Person . class . getAnnotation ( Hint . class );
System . out . println ( hint ); // null
Hints hints1 = Person . class . getAnnotation ( Hints . class );
System . out . println ( hints1 . value (). length ); // 2
Hint [] hints2 = Person . class . getAnnotationsByType ( Hint . class );
System . out . println ( hints2 . length ); // 2
على الرغم من أننا لم نعلن مطلقًا عن التعليق التوضيحي @Hints
في فئة Person
، إلا أنه لا يزال قابلاً للقراءة عبر getAnnotation(Hints.class)
. ومع ذلك، فإن الطريقة الأكثر ملاءمة هي getAnnotationsByType
التي تمنح الوصول المباشر إلى جميع التعليقات التوضيحية @Hint
المشروحة.
علاوة على ذلك، تم توسيع استخدام التعليقات التوضيحية في Java 8 ليشمل هدفين جديدين:
@ Target ({ ElementType . TYPE_PARAMETER , ElementType . TYPE_USE })
@interface MyAnnotation {}
دليل البرمجة الخاص بي إلى Java 8 ينتهي هنا. إذا كنت تريد معرفة المزيد حول جميع الفئات والميزات الجديدة لـ JDK 8 API، فراجع JDK8 API Explorer الخاص بي. يساعدك على اكتشاف جميع الفئات الجديدة والجواهر المخفية في JDK 8، مثل Arrays.parallelSort
و StampedLock
و CompletableFuture
- على سبيل المثال لا الحصر.
لقد قمت أيضًا بنشر مجموعة من المقالات اللاحقة على مدونتي والتي قد تكون مثيرة للاهتمام بالنسبة لك:
يجب عليك متابعتي على تويتر. شكرا للقراءة!