“Effective Java (3rd Edition)” থেকে Best Practices
Joshua Bloch-এর “Effective Java (3rd Edition) ” থেকে সংকলিত
১. null নয়, খালি collection বা array রিটার্ন করুন
মেথড যদি একটি Set রিটার্ন করে, তবে খালি ফলাফলের জন্য return null; নয় – বরং return Set.of(); লিখুন।
২. কনস্ট্রাক্টরের বদলে static factory মেথডের কথা ভাবুন
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
সবসময় ভালো নয়, তবে কিছু ক্ষেত্রে এদের অনেক সুবিধা আছে:
- static factory মেথডের একটি সুবিধা হলো, কনস্ট্রাক্টরের মতো নয়, এদের নাম থাকে।
- প্রতিবার কল করার সময় এদের নতুন অবজেক্ট তৈরি করতে হয় না।
- এরা নিজেদের রিটার্ন টাইপের যেকোনো সাবটাইপের অবজেক্ট রিটার্ন করতে পারে।
- ইনপুট প্যারামিটারের ওপর নির্ভর করে রিটার্ন করা অবজেক্টের ক্লাস কল থেকে কলে বদলাতে পারে।
- মেথডটি যে ক্লাসে আছে, সেটি লেখার সময় রিটার্ন করা অবজেক্টের ক্লাসের অস্তিত্ব থাকারও দরকার নেই।
অসুবিধা:
- শুধু static factory মেথড দেওয়ার প্রধান সীমাবদ্ধতা হলো, public বা protected কনস্ট্রাক্টর ছাড়া ক্লাসের সাবক্লাস তৈরি করা যায় না।
- static factory মেথডের দ্বিতীয় দুর্বলতা হলো, প্রোগ্রামারদের পক্ষে এগুলো খুঁজে পাওয়া কঠিন।
৩. অনেক কনস্ট্রাক্টর প্যারামিটার থাকলে builder-এর কথা ভাবুন
static factory আর কনস্ট্রাক্টরের একটি সাধারণ সীমাবদ্ধতা আছে: অনেকগুলো ঐচ্ছিক প্যারামিটার থাকলে এরা ভালোভাবে মানিয়ে নিতে পারে না। আমার কাজ করা কিছু প্রজেক্টে এই সমস্যা খুব জোরালোভাবে দেখা দিয়েছে।
৪. private কনস্ট্রাক্টর বা enum টাইপ দিয়ে singleton বৈশিষ্ট্য নিশ্চিত করুন
যার একাধিক ইনস্ট্যান্সের দরকার নেই, তা শুধু একবারই ইনস্ট্যানশিয়েট করা উচিত – @ApplicationScoped দিয়ে হোক বা অন্য কোনো singleton প্যাটার্নে।
৫. private কনস্ট্রাক্টর দিয়ে ইনস্ট্যানশিয়েশন বন্ধ করুন
মাঝে মাঝে আপনি এমন একটি ক্লাস লিখতে চাইবেন, যা কেবল কিছু static মেথড আর static ফিল্ডের সমষ্টি। এমন ক্লাসের দুর্নাম আছে, কারণ কেউ কেউ অবজেক্টের ভাষায় চিন্তা করা এড়াতে এদের অপব্যবহার করেন, তবে এদের যথার্থ ব্যবহারও আছে।
private MyStaticClass() {
// private constructor to prevent instantiation
}
৬. অপ্রয়োজনীয় অবজেক্ট তৈরি এড়িয়ে চলুন
String s = new String("bikini"); // DON'T DO THIS!
অথবা:
// Hideously slow! Can you spot the object creation?
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
সমাধান: এই প্রোগ্রাম সঠিক উত্তর দেয়, কিন্তু মাত্র একটি অক্ষরের টাইপোর কারণে যতটা দ্রুত হওয়া উচিত তার চেয়ে অনেক ধীর। ভেরিয়েবল sum-কে long-এর বদলে Long হিসেবে ঘোষণা করা হয়েছে, ফলে প্রোগ্রামটি প্রায় 2^31টি অপ্রয়োজনীয় Long ইনস্ট্যান্স তৈরি করে (মোটামুটি প্রতিবার long i-কে Long sum-এর সাথে যোগ করার সময় একটি করে)।
৭. finalizer ও cleaner এড়িয়ে চলুন
Finalizer অনির্দেশ্য, প্রায়ই বিপজ্জনক এবং সাধারণত অপ্রয়োজনীয়। এদের ব্যবহারে অস্থির আচরণ, দুর্বল পারফরম্যান্স এবং portability-র সমস্যা দেখা দিতে পারে।
৮. try-finally-র চেয়ে try-with-resources বেছে নিন
// try-with-resources - the best way to close resources!
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
৯. Comparable ইমপ্লিমেন্ট করার কথা ভাবুন
Comparable ইমপ্লিমেন্ট করলে আপনার ক্লাস সেইসব অসংখ্য generic অ্যালগরিদম ও collection ইমপ্লিমেন্টেশনের সাথে কাজ করতে পারে, যেগুলো এই ইন্টারফেসের ওপর নির্ভর করে।
১০. ক্লাস ও member-এর অ্যাক্সেসিবিলিটি যতটা সম্ভব কমান
একটি ভালোভাবে ডিজাইন করা কম্পোনেন্টকে দুর্বলভাবে ডিজাইন করা কম্পোনেন্ট থেকে আলাদা করার সবচেয়ে গুরুত্বপূর্ণ বিষয় হলো, কম্পোনেন্টটি তার অভ্যন্তরীণ ডেটা ও অন্যান্য ইমপ্লিমেন্টেশনের খুঁটিনাটি অন্য কম্পোনেন্ট থেকে কতটা লুকিয়ে রাখে। একটি ভালোভাবে ডিজাইন করা কম্পোনেন্ট তার সব ইমপ্লিমেন্টেশনের খুঁটিনাটি লুকিয়ে রাখে এবং তার API-কে ইমপ্লিমেন্টেশন থেকে পরিষ্কারভাবে আলাদা করে। তখন কম্পোনেন্টগুলো কেবল নিজেদের API-র মাধ্যমে যোগাযোগ করে এবং একে অপরের ভেতরের কার্যপ্রণালী সম্পর্কে কিছুই জানে না। information hiding বা encapsulation নামে পরিচিত এই ধারণা সফটওয়্যার ডিজাইনের একটি মৌলিক নীতি।
১১. public ক্লাসে public ফিল্ড নয়, accessor মেথড ব্যবহার করুন
মাঝে মাঝে আপনার এমন অবনমিত (degenerate) ক্লাস লেখার লোভ হতে পারে, যার কাজ কেবল কিছু instance ফিল্ডকে একসাথে রাখা:
// Degenerate classes like this should not be public!
class Point {
public double x;
public double y;
}
এ ধরনের ক্লাসের ডেটা ফিল্ড সরাসরি অ্যাক্সেস করা হয় বলে এরা encapsulation-এর সুবিধা দেয় না। API না বদলে আপনি এর রিপ্রেজেন্টেশন বদলাতে পারবেন না, invariant বজায় রাখতে পারবেন না, এবং কোনো ফিল্ড অ্যাক্সেস হলে বাড়তি কোনো পদক্ষেপও নিতে পারবেন না। কট্টর অবজেক্ট-ওরিয়েন্টেড প্রোগ্রামাররা মনে করেন, এমন ক্লাস একেবারে বর্জনীয় এবং সবসময় এদের বদলে private ফিল্ড ও public accessor মেথড (getter) এবং mutable ক্লাসের ক্ষেত্রে mutator (setter) সহ ক্লাস ব্যবহার করা উচিত:
// Encapsulation of data by accessor methods and mutators
class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public void setX(double x) { this.x = x; }
public void setY(double y) { this.y = y; }
}
public ক্লাসের ব্যাপারে কট্টরপন্থীরা অবশ্যই ঠিক: কোনো ক্লাস যদি তার প্যাকেজের বাইরে থেকে অ্যাক্সেসযোগ্য হয়, তবে accessor মেথড দিন, যাতে ক্লাসের অভ্যন্তরীণ রিপ্রেজেন্টেশন বদলানোর নমনীয়তা বজায় থাকে। কোনো public ক্লাস যদি তার ডেটা ফিল্ড উন্মুক্ত করে দেয়, তবে রিপ্রেজেন্টেশন বদলানোর সব আশা শেষ, কারণ ক্লায়েন্ট কোড চারদিকে ছড়িয়ে পড়তে পারে।
তবে কোনো ক্লাস যদি package-private হয় বা private nested ক্লাস হয়, তবে তার ডেটা ফিল্ড উন্মুক্ত রাখায় মৌলিকভাবে কোনো দোষ নেই – যদি সেগুলো ক্লাসটির দেওয়া abstraction যথাযথভাবে বর্ণনা করে।
১২. mutability যতটা সম্ভব কমান
একটি immutable ক্লাস হলো এমন একটি ক্লাস, যার ইনস্ট্যান্স পরিবর্তন করা যায় না। প্রতিটি ইনস্ট্যান্সে থাকা সব তথ্য অবজেক্টের পুরো জীবনকাল ধরে স্থির থাকে, তাই কোনো পরিবর্তন কখনো দেখা যায় না। Java প্ল্যাটফর্ম লাইব্রেরিতে অনেক immutable ক্লাস আছে, যেমন String, boxed primitive ক্লাসগুলো, BigInteger ও BigDecimal। এর পেছনে অনেক ভালো কারণ আছে: immutable ক্লাস mutable ক্লাসের চেয়ে ডিজাইন, ইমপ্লিমেন্ট ও ব্যবহার করা সহজ। এগুলোতে ভুলের সম্ভাবনা কম এবং এগুলো বেশি নিরাপদ। একটি ক্লাসকে immutable করতে এই পাঁচটি নিয়ম মেনে চলুন:
- অবজেক্টের state পরিবর্তন করে এমন মেথড দেবেন না (যাদের mutator বলা হয়)।
-
নিশ্চিত করুন যে ক্লাসটি extend করা যাবে না। এতে অসতর্ক বা ক্ষতিকর সাবক্লাস অবজেক্টের state বদলে গেছে এমন আচরণ করে ক্লাসের immutable আচরণ নষ্ট করতে পারে না।
সাবক্লাস তৈরি ঠেকানোর সাধারণ উপায় হলো, যেমন, ক্লাসটিকে
finalকরে দেওয়া। - সব ফিল্ড final করুন। এতে আপনার উদ্দেশ্য এমনভাবে স্পষ্ট হয়, যা সিস্টেম নিজেই বলবৎ করে। তাছাড়া memory model-এ যেমন বলা আছে, নতুন তৈরি কোনো ইনস্ট্যান্সের রেফারেন্স synchronization ছাড়াই এক থ্রেড থেকে অন্য থ্রেডে পাঠানো হলে সঠিক আচরণ নিশ্চিত করতেও এটি দরকার।
- সব ফিল্ড private করুন। এতে ক্লায়েন্টরা ফিল্ডের রেফার করা mutable অবজেক্টের অ্যাক্সেস পেয়ে সেগুলো সরাসরি বদলে ফেলতে পারে না। immutable ক্লাসে primitive ভ্যালু বা immutable অবজেক্টের রেফারেন্সধারী public final ফিল্ড রাখা টেকনিক্যালি অনুমোদিত হলেও এটি সুপারিশ করা হয় না, কারণ এতে পরবর্তী রিলিজে অভ্যন্তরীণ রিপ্রেজেন্টেশন বদলানোর পথ বন্ধ হয়ে যায়।
-
যেকোনো mutable কম্পোনেন্টে একচেটিয়া অ্যাক্সেস নিশ্চিত করুন। আপনার ক্লাসে যদি mutable অবজেক্টের রেফারেন্সধারী কোনো ফিল্ড থাকে, তবে নিশ্চিত করুন যে ক্লাসের ক্লায়েন্টরা
এই অবজেক্টগুলোর রেফারেন্স পেতে না পারে। এমন ফিল্ডকে কখনো ক্লায়েন্টের দেওয়া অবজেক্ট রেফারেন্স দিয়ে initialize করবেন না, বা কোনো accessor থেকে ফিল্ডটি রিটার্ন করবেন না। কনস্ট্রাক্টর, accessor এবং
readObjectমেথডে defensive copy তৈরি করুন।
১৩. ইনহেরিটেন্সের চেয়ে কম্পোজিশনকে প্রাধান্য দিন
ইনহেরিটেন্স কোড পুনর্ব্যবহারের একটি শক্তিশালী উপায়, কিন্তু সবসময় এটি কাজের জন্য সেরা টুল নয়। ভুলভাবে ব্যবহার করলে এটি ভঙ্গুর সফটওয়্যার তৈরি করে। একটি প্যাকেজের ভেতরে ইনহেরিটেন্স ব্যবহার নিরাপদ, যেখানে সাবক্লাস ও সুপারক্লাসের ইমপ্লিমেন্টেশন একই প্রোগ্রামারদের নিয়ন্ত্রণে থাকে। যেসব ক্লাস বিশেষভাবে extend করার জন্য ডিজাইন ও ডকুমেন্ট করা হয়েছে, সেগুলো extend করার ক্ষেত্রেও ইনহেরিটেন্স নিরাপদ। তবে প্যাকেজের সীমানা পেরিয়ে সাধারণ concrete ক্লাস থেকে ইনহেরিট করা বিপজ্জনক।
মেথড কলের বিপরীতে, ইনহেরিটেন্স encapsulation ভঙ্গ করে। আপনি ভাবতে পারেন, শুধু নতুন মেথড যোগ করলে এবং বিদ্যমান মেথড override না করলে কোনো ক্লাস extend করা নিরাপদ। এ ধরনের extension অনেক বেশি নিরাপদ হলেও ঝুঁকিমুক্ত নয়। পরবর্তী কোনো রিলিজে সুপারক্লাসে যদি নতুন একটি মেথড যোগ হয়, আর দুর্ভাগ্যবশত আপনি সাবক্লাসে একই signature কিন্তু ভিন্ন রিটার্ন টাইপের একটি মেথড দিয়ে থাকেন, তবে আপনার সাবক্লাস আর কম্পাইলই হবে না। আর যদি সাবক্লাসে নতুন সুপারক্লাস মেথডের মতোই signature ও রিটার্ন টাইপের মেথড দিয়ে থাকেন, তবে আপনি এখন সেটি override করছেন, ফলে আগে বর্ণিত সমস্যাগুলোর মুখে পড়বেন। তাছাড়া আপনার মেথড নতুন সুপারক্লাস মেথডের contract পূরণ করবে কি না, তা নিয়ে সন্দেহ আছে, কারণ আপনি যখন সাবক্লাস মেথডটি লিখেছিলেন, তখন সেই contract লেখাই হয়নি।
সৌভাগ্যক্রমে, উপরের সব সমস্যা এড়ানোর একটি উপায় আছে। বিদ্যমান ক্লাস extend করার বদলে আপনার নতুন ক্লাসে একটি private ফিল্ড দিন, যা বিদ্যমান ক্লাসের একটি ইনস্ট্যান্সকে রেফার করে। এই ডিজাইনকে কম্পোজিশন বলা হয়, কারণ বিদ্যমান ক্লাসটি নতুন ক্লাসের একটি কম্পোনেন্টে পরিণত হয়। নতুন ক্লাসের প্রতিটি instance মেথড ভেতরে থাকা বিদ্যমান ক্লাসের ইনস্ট্যান্সে সংশ্লিষ্ট মেথডটি কল করে এবং ফলাফল রিটার্ন করে। একে forwarding বলা হয়, আর নতুন ক্লাসের মেথডগুলোকে বলা হয় forwarding মেথড। এর ফলে যে ক্লাস পাওয়া যায় তা পাথরের মতো মজবুত, বিদ্যমান ক্লাসের ইমপ্লিমেন্টেশনের খুঁটিনাটির ওপর কোনো নির্ভরতা নেই। এমনকি বিদ্যমান ক্লাসে নতুন মেথড যোগ হলেও নতুন ক্লাসের ওপর কোনো প্রভাব পড়বে না।
১৪. abstract ক্লাসের চেয়ে ইন্টারফেসকে প্রাধান্য দিন
একাধিক ইমপ্লিমেন্টেশন অনুমোদন করে এমন টাইপ সংজ্ঞায়িত করার জন্য Java-তে দুটি পদ্ধতি আছে: ইন্টারফেস ও abstract ক্লাস। Java 8-এ ইন্টারফেসের জন্য default মেথড চালু হওয়ার পর থেকে দুটি পদ্ধতিতেই কিছু instance মেথডের ইমপ্লিমেন্টেশন দেওয়া যায়।
একটি বড় পার্থক্য হলো, abstract ক্লাস দিয়ে সংজ্ঞায়িত টাইপ ইমপ্লিমেন্ট করতে হলে একটি ক্লাসকে অবশ্যই সেই abstract ক্লাসের সাবক্লাস হতে হবে। যেহেতু Java কেবল single inheritance অনুমোদন করে, abstract ক্লাসের এই সীমাবদ্ধতা টাইপ সংজ্ঞা হিসেবে এদের ব্যবহারকে মারাত্মকভাবে সীমিত করে। অন্যদিকে, যে ক্লাস প্রয়োজনীয় সব মেথড সংজ্ঞায়িত করে এবং general contract মেনে চলে, সেটি ক্লাস হায়ারার্কির যেখানেই থাকুক না কেন, একটি ইন্টারফেস ইমপ্লিমেন্ট করতে পারে। বিদ্যমান ক্লাসগুলোতে সহজেই একটি নতুন ইন্টারফেস ইমপ্লিমেন্ট করিয়ে নেওয়া যায়।
- mixin সংজ্ঞায়িত করার জন্য ইন্টারফেস আদর্শ।
- ইন্টারফেস দিয়ে hierarchy-বিহীন টাইপ ফ্রেমওয়ার্ক তৈরি করা যায়।
- ইন্টারফেস নিরাপদ ও শক্তিশালী ফাংশনালিটি সম্প্রসারণ সম্ভব করে – wrapper class idiom-এর মাধ্যমে। টাইপ সংজ্ঞায়িত করতে abstract ক্লাস ব্যবহার করলে, যে প্রোগ্রামার ফাংশনালিটি যোগ করতে চান, তার সামনে ইনহেরিটেন্স ছাড়া আর কোনো পথ খোলা থাকে না।
১৫. Generics: raw type ব্যবহার করবেন না
ত্রুটিটি আপনি runtime-এর আগে টের পাবেন না – অর্থাৎ ঘটে যাওয়ার অনেক পরে, এবং এমন কোডে, যা ত্রুটিযুক্ত কোড থেকে অনেক দূরে হতে পারে। ClassCastException দেখার পর আপনাকে পুরো codebase ঘেঁটে সেই মেথড কলটি খুঁজতে হবে, যেটি ডাকটিকিটের সংগ্রহে মুদ্রাটি ঢুকিয়েছিল। কম্পাইলার আপনাকে সাহায্য করতে পারবে না।
১৬. Generics: unchecked warning দূর করুন
generics দিয়ে প্রোগ্রাম লিখলে আপনি অনেক কম্পাইলার warning দেখবেন:
- unchecked cast warning
- unchecked method invocation warning
- unchecked parameterized vararg type warning
- unchecked conversion warning।
generics নিয়ে আপনার অভিজ্ঞতা যত বাড়বে, warning তত কমবে, তবে নতুন লেখা কোড একেবারে পরিষ্কারভাবে কম্পাইল হবে – এমন আশা করবেন না। অনেক unchecked warning সহজেই দূর করা যায়। যেমন ধরুন, আপনি ভুলবশত এই ডিক্লারেশনটি লিখলেন:
Set<Lark> exaltation = new HashSet();
এটিকে বদলে লিখুন:
Set<Lark> exaltation = new HashSet<>();
কিছু warning দূর করা অনেক বেশি কঠিন হবে। যেসব warning নিয়ে একটু ভাবতে হয়, সেগুলোর ক্ষেত্রে হাল ছাড়বেন না! যতগুলো unchecked warning দূর করতে পারেন, সবগুলো দূর করুন।
সব warning দূর করলে আপনি নিশ্চিত থাকতে পারেন যে আপনার কোড typesafe, যা খুবই ভালো ব্যাপার। এর মানে runtime-এ আপনি কোনো ClassCastException পাবেন না, আর আপনার প্রোগ্রাম আপনার ইচ্ছামতো
আচরণ করবে – এই আস্থাও বাড়ে। যদি কোনো warning দূর করতে না পারেন, কিন্তু প্রমাণ করতে পারেন যে warning-সৃষ্টিকারী কোডটি typesafe, তবেই (এবং কেবল তখনই) একটি
@SuppressWarnings("unchecked") annotation দিয়ে warning-টি suppress করুন। কোডটি typesafe তা আগে প্রমাণ না করে warning suppress করলে আপনি নিজেকে নিরাপত্তার
একটি মিথ্যা অনুভূতি দিচ্ছেন।
কোডটি কোনো warning ছাড়াই কম্পাইল হতে পারে, তবুও runtime-এ ClassCastException ছুড়তে পারে।
প্রতিবার @SuppressWarnings("unchecked") annotation ব্যবহার করলে একটি কমেন্ট যোগ করুন, কেন এটি করা নিরাপদ।
১৭. Generics: যখন এটি অর্থবহ
উফ – এমন একটি ক্লাস, যার generics-এর খুব দরকার:
এ ধরনের জিনিস সবচেয়ে দেরিতে হলেও merge request-এ ধরা পড়া উচিত!
১৮. Generics: যেভাবে করবেন না
দুঃখিত, আর কিছু বলার দরকার নেই। দয়া করে এটা কখনোই merge করবেন না:
public abstract class JpaLearningCardBaseContentService<LC extends LearningCardBaseContent<L, LC, AC>, J extends JpaLearningCardBaseContent<L, LC, AC>, L extends
LearningCardBase<LC, A>, A extends AnswerOptionBase<L, AC>, AC extends AnswerOptionBaseContent<LC, AC, A>, AS extends AnswerOptionBaseService<A, AC, L, LC>, ACS extends
AnswerOptionBaseContentService<AC, A, L, LC>, LS extends LearningCardBaseService<L>>
extends JpaLightPersistenceService<LC, J, Long> implements LearningCardBaseContentService<LC, L, A, AC> {
// ...
}
শুধু একটি ক্লাস ডিক্লারেশন বুঝতে কখনো ৫ মিনিট লাগা উচিত নয়।
১৯. array-র চেয়ে list-কে প্রাধান্য দিন
array দুটি গুরুত্বপূর্ণ দিক থেকে generic টাইপ থেকে আলাদা। প্রথমত, array covariant। এই ভীতিকর শোনানো শব্দটির অর্থ কেবল এই যে, Sub যদি Super-এর সাবটাইপ হয়, তবে
array টাইপ Sub[]-ও array টাইপ Super[]-এর সাবটাইপ। অন্যদিকে generics হলো invariant: যেকোনো দুটি ভিন্ন টাইপ Type1 ও Type2-এর জন্য
List<Type1>, List<Type2>-এর সাবটাইপও নয়, সুপারটাইপও নয়।
২০. anonymous ক্লাসের চেয়ে lambda-কে প্রাধান্য দিন
// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
বরং এভাবে করুন:
// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
২১. লোকাল ভেরিয়েবলের scope যতটা সম্ভব ছোট রাখুন
লোকাল ভেরিয়েবলের scope ছোট রাখার সবচেয়ে কার্যকর কৌশল হলো, যেখানে এটি প্রথম ব্যবহৃত হয় সেখানেই এটি ডিক্লেয়ার করা...
২২. প্রচলিত for লুপের চেয়ে for-each লুপকে প্রাধান্য দিন
// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
... // Do something with e
}
২৩. লাইব্রেরিগুলো জানুন ও ব্যবহার করুন
ধরুন আপনি শূন্য থেকে একটি নির্দিষ্ট ঊর্ধ্বসীমার মধ্যে র্যান্ডম integer তৈরি করতে চান। এই সাধারণ কাজের মুখোমুখি হয়ে অনেক প্রোগ্রামার এরকম একটি ছোট মেথড লিখে ফেলবেন:
// Common but deeply flawed!
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}
মেথডটি দেখতে ভালো হলেও এতে তিনটি ত্রুটি আছে। প্রথমটি হলো, n যদি দুইয়ের একটি ছোট ঘাত হয়, তবে র্যান্ডম সংখ্যার ক্রম বেশ অল্প সময় পরেই পুনরাবৃত্তি হবে। দ্বিতীয় ত্রুটি হলো, n যদি দুইয়ের ঘাত না হয়, তবে গড়ে কিছু সংখ্যা অন্যগুলোর চেয়ে বেশিবার রিটার্ন হবে। n বড় হলে এই প্রভাব বেশ প্রকট হতে পারে।
random মেথডের তৃতীয় ত্রুটি হলো, বিরল ক্ষেত্রে এটি ভয়াবহভাবে ব্যর্থ হতে পারে এবং নির্দিষ্ট পরিসীমার বাইরের একটি সংখ্যা রিটার্ন করতে পারে। এর কারণ, মেথডটি
rnd.nextInt()-এর রিটার্ন করা ভ্যালুকে একটি non-negative int-এ রূপান্তর করতে Math.abs কল করে। nextInt() যদি
Integer.MIN_VALUE রিটার্ন করে, তবে Math.abs-ও Integer.MIN_VALUE রিটার্ন করবে, এবং ভাগশেষ অপারেটর (%) একটি ঋণাত্মক সংখ্যা রিটার্ন করবে – ধরে নিয়ে যে
n দুইয়ের ঘাত নয়। এতে প্রায় নিশ্চিতভাবেই আপনার প্রোগ্রাম ব্যর্থ হবে, আর সেই ব্যর্থতা পুনরায় ঘটানো কঠিন হতে পারে।
২৪. নির্ভুল উত্তর দরকার হলে float ও double এড়িয়ে চলুন
টাকার অঙ্কের জন্য BigDecimal ব্যবহার করুন। float ও double রাউন্ডিং ত্রুটি তৈরি করে।
উদাহরণস্বরূপ, ধরুন আপনার পকেটে $1.03 আছে, আর আপনি 42¢ খরচ করলেন। আপনার কাছে কত টাকা রইল? এই প্রশ্নের উত্তর দেওয়ার চেষ্টা করে এমন একটি সরল প্রোগ্রাম অংশ:
System.out.println(1.03 - 0.42);
দুর্ভাগ্যবশত এটি 0.6100000000000001 প্রিন্ট করে। এটি কোনো বিচ্ছিন্ন ঘটনা নয়। ধরুন আপনার পকেটে এক ডলার আছে, আর আপনি দশ সেন্ট করে দামের নয়টি ওয়াশার কিনলেন। আপনি কত
ফেরত পাবেন?
System.out.println(1.00 - 9 * 0.10);
এই প্রোগ্রাম অংশ অনুযায়ী আপনি পাবেন $0.09999999999999998।
আপনি ভাবতে পারেন, প্রিন্ট করার আগে ফলাফল রাউন্ড করলেই সমস্যার সমাধান হবে, কিন্তু দুর্ভাগ্যবশত তা সবসময় কাজ করে না। যেমন ধরুন, আপনার পকেটে এক ডলার আছে, আর আপনি একটি তাকে সারি সারি সুস্বাদু ক্যান্ডি দেখলেন, যাদের দাম 10¢, 20¢, 30¢ – এভাবে এক ডলার পর্যন্ত। আপনি 10¢-এর ক্যান্ডিটি দিয়ে শুরু করে প্রতিটি থেকে একটি করে কিনতে থাকলেন, যতক্ষণ না তাকের পরের ক্যান্ডিটি কেনার সামর্থ্য ফুরিয়ে যায়।
২৫. boxed primitive-এর চেয়ে primitive টাইপকে প্রাধান্য দিন
// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
২৬. অবজেক্টকে তাদের ইন্টারফেস দিয়ে রেফার করুন
ভালো উদাহরণ:
Set<Son> sonSet = new LinkedHashSet<>();
খারাপ উদাহরণ:
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
২৭. reflection-এর চেয়ে ইন্টারফেসকে প্রাধান্য দিন
নইলে:
- কম্পাইল-টাইম টাইপ চেকিংয়ের সব সুবিধা হারাবেন, exception checking-সহ। কোনো প্রোগ্রাম যদি reflection দিয়ে অস্তিত্বহীন বা অ্যাক্সেস-অযোগ্য কোনো মেথড কল করার চেষ্টা করে, তবে বিশেষ সতর্কতা না নিলে এটি runtime-এ ব্যর্থ হবে।
- reflection দিয়ে অ্যাক্সেস করার কোড আনাড়ি ও অতিরিক্ত লম্বা। এটি লেখা ক্লান্তিকর এবং পড়া কঠিন।
- পারফরম্যান্স ক্ষতিগ্রস্ত হয়। reflection দিয়ে মেথড কল সাধারণ মেথড কলের চেয়ে অনেক ধীর।
২৮. সর্বজনস্বীকৃত naming convention মেনে চলুন
Java প্ল্যাটফর্মের naming convention-এর একটি সুপ্রতিষ্ঠিত সেট আছে, যার অনেকগুলোই The Java Language Specification [JLS, 6.1]-এ রয়েছে।
২৯. exception কেবল ব্যতিক্রমী পরিস্থিতিতে ব্যবহার করুন
// Horrible abuse of exceptions. Don't ever do this!
try {
int i = 0;
while (true) {
range[i++].climb();
}
} catch (ArrayIndexOutOfBoundsException e) {
// catch it
}
এই কোড কী করে? দেখে মোটেই বোঝা যায় না, আর এটাই একে ব্যবহার না করার জন্য যথেষ্ট কারণ। দেখা যায়, এটি একটি array-র এলিমেন্টগুলোর ওপর লুপ চালানোর এক ভয়ানক ভুল-ভাবনার
idiom। অসীম লুপটি array-র সীমানার বাইরের প্রথম এলিমেন্টটি অ্যাক্সেস করার চেষ্টা করার সময় একটি ArrayIndexOutOfBoundsException ছুড়ে, ধরে এবং উপেক্ষা করে
শেষ হয়। এটি array-তে লুপ চালানোর স্ট্যান্ডার্ড idiom-এর সমতুল্য হওয়ার কথা, যা যেকোনো Java প্রোগ্রামার সঙ্গে সঙ্গে চিনে ফেলেন:
for (Mountain m : range) {
m.climb();
}
নাম থেকেই বোঝা যায়, exception কেবল ব্যতিক্রমী পরিস্থিতির জন্য; সাধারণ control flow-এর জন্য এগুলো কখনোই ব্যবহার করা উচিত নয়।
৩০. পুনরুদ্ধারযোগ্য পরিস্থিতির জন্য checked exception এবং প্রোগ্রামিং ত্রুটির জন্য runtime exception ব্যবহার করুন
যেসব পরিস্থিতি থেকে কলারের পুনরুদ্ধার করতে পারার যুক্তিসঙ্গত আশা আছে, সেগুলোর জন্য checked exception ব্যবহার করুন। ব্যবহারকারীকে checked exception-এর মুখোমুখি করে API ডিজাইনার পরিস্থিতি থেকে পুনরুদ্ধারের একটি বাধ্যবাধকতা চাপিয়ে দেন। ব্যবহারকারী exception ধরে উপেক্ষা করে এই বাধ্যবাধকতা অগ্রাহ্য করতে পারেন, কিন্তু সাধারণত সেটা খারাপ ধারণা।
প্রোগ্রামিং ত্রুটি বোঝাতে runtime exception ব্যবহার করুন। বেশিরভাগ runtime exception precondition লঙ্ঘন নির্দেশ করে। precondition লঙ্ঘন মানে কেবল এই যে, API-র
ক্লায়েন্ট API স্পেসিফিকেশনে নির্ধারিত contract মেনে চলেনি। যেমন, array অ্যাক্সেসের contract বলে যে array index অবশ্যই শূন্য থেকে array-র দৈর্ঘ্য বিয়োগ এক পর্যন্ত (উভয়সহ)
হতে হবে। ArrayIndexOutOfBoundsException নির্দেশ করে যে এই precondition লঙ্ঘিত হয়েছে।
৩১. checked exception-এর অপ্রয়োজনীয় ব্যবহার এড়িয়ে চলুন
সংক্ষেপে, পরিমিতভাবে ব্যবহার করলে checked exception প্রোগ্রামের নির্ভরযোগ্যতা বাড়াতে পারে; অতিরিক্ত ব্যবহার করলে API ব্যবহার করা যন্ত্রণাদায়ক হয়ে ওঠে।
Pair<Boolean, LearningCardGroup> answerLearningCard(@NotNull LearningCardGroup learningCardGroup,
LearningCardAssignment learningCardAssignment,
@NotNull Collection<AnswerOption> answers)
throws InvalidTrainingStateException, PersistenceException, LearningCardGroupAlreadyCompletedException,
ConsecutiveCorrectCountExceededException,
PreviousLearningCardGroupUncompletedException, LearningCardGroupCooldownException,
LearningCardGroupHasUncompletedLearningCardsException, InvalidParticipantException, TrainingNotSignedException; // but why ???
// this is a mistake seen a lot with junior devs
// exceptions are exceptionally slow and should be avoided for mere validation
// the performance gets even worse if a stacktrace is printed, since logging and printing is one of the slowest possible actions
৩২. স্ট্যান্ডার্ড exception ব্যবহারকে প্রাধান্য দিন
দক্ষ প্রোগ্রামারদের কম অভিজ্ঞদের থেকে আলাদা করে যে বৈশিষ্ট্য, তা হলো দক্ষরা উচ্চমাত্রার কোড পুনর্ব্যবহারের চেষ্টা করেন এবং সাধারণত তা অর্জনও করেন।
কোড পুনর্ব্যবহার যে ভালো, এই নিয়মের ব্যতিক্রম exception-ও নয়। Java লাইব্রেরিগুলো এমন একসেট exception দেয়, যা বেশিরভাগ API-র exception ছোড়ার বেশিরভাগ প্রয়োজন মেটায়। স্ট্যান্ডার্ড exception পুনর্ব্যবহারের বেশ কিছু সুবিধা আছে। এর মধ্যে প্রধান হলো, এটি আপনার API শেখা ও ব্যবহার সহজ করে, কারণ এটি প্রোগ্রামারদের আগে থেকে পরিচিত প্রতিষ্ঠিত রীতির সাথে মেলে। প্রায় সমান গুরুত্বপূর্ণ দ্বিতীয় সুবিধা হলো, আপনার API ব্যবহারকারী প্রোগ্রাম পড়া সহজ হয়, কারণ সেগুলো অপরিচিত exception-এ ভরা থাকে না। সবশেষে (এবং সবচেয়ে কম গুরুত্বপূর্ণ), কম exception ক্লাস মানে কম memory footprint এবং ক্লাস লোড করতে কম সময়।
সবচেয়ে বেশি পুনর্ব্যবহৃত exception টাইপ হলো IllegalArgumentException। কলার যখন এমন আর্গুমেন্ট পাঠায় যার ভ্যালু অনুপযুক্ত, তখন সাধারণত এই exception ছোড়া হয়। যেমন,
কোনো কাজ কতবার পুনরাবৃত্তি হবে তা বোঝানো প্যারামিটারে কলার যদি ঋণাত্মক সংখ্যা পাঠায়, তবে এই exception ছোড়া উচিত। আরেকটি বহুল পুনর্ব্যবহৃত exception হলো
IllegalStateException। যে অবজেক্টকে কল করা হচ্ছে তার state-এর কারণে কলটি অবৈধ হলে সাধারণত এই exception ছোড়া হয়। যেমন, কলার যদি কোনো অবজেক্ট সঠিকভাবে
initialize হওয়ার আগেই ব্যবহার করার চেষ্টা করে, তবে এই exception ছোড়া উচিত।
যুক্তি দেওয়া যায় যে, প্রতিটি ভুল মেথড কলই শেষ পর্যন্ত একটি অবৈধ আর্গুমেন্ট বা state-এ গিয়ে দাঁড়ায়, তবে নির্দিষ্ট ধরনের অবৈধ আর্গুমেন্ট ও state-এর জন্য প্রথাগতভাবে অন্য exception
ব্যবহার করা হয়। কলার যদি এমন কোনো প্যারামিটারে null পাঠায় যেখানে null ভ্যালু নিষিদ্ধ, তবে রীতি অনুযায়ী IllegalArgumentException-এর বদলে NullPointerException ছোড়া উচিত।
একইভাবে, কোনো sequence-এর index বোঝানো প্যারামিটারে কলার যদি সীমার বাইরের ভ্যালু পাঠায়, তবে IllegalArgumentException-এর বদলে IndexOutOfBoundsException ছোড়া উচিত।
Exception, RuntimeException, Throwable বা Error সরাসরি পুনর্ব্যবহার করবেন না। এই ক্লাসগুলোকে abstract হিসেবে বিবেচনা করুন। তবে কোনো checked exception-কে runtime exception-এ
রূপান্তর করতে চাইলে RuntimeException ব্যবহার করা যেতে পারে – যদি আপনি সত্যিই জানেন আপনি কী করছেন।
৩৩. exception উপেক্ষা করবেন না
// Empty catch block ignores exception - Highly suspect!
try {
...
} catch (SomeException e) {
}
খালি catch ব্লক exception-এর উদ্দেশ্যকেই ব্যর্থ করে দেয়, আর সেই উদ্দেশ্য হলো আপনাকে ব্যতিক্রমী পরিস্থিতি সামলাতে বাধ্য করা। কোনো exception উপেক্ষা করা অনেকটা ফায়ার অ্যালার্ম উপেক্ষা করার মতো – এবং সেটি বন্ধ করে দেওয়ার মতো, যাতে আর কেউ দেখার সুযোগই না পায় যে সত্যিই আগুন লেগেছে কি না।