Tôi nghĩ tôi sẽ tặng quả bóng mềm này cho bất kỳ ai muốn đánh nó ra khỏi công viên. Thuốc generic là gì, ưu điểm của thuốc generic là gì, tại sao, ở đâu, tôi nên sử dụng chúng như thế nào? Hãy giữ nó khá cơ bản. Cảm ơn.
Tôi nghĩ tôi sẽ tặng quả bóng mềm này cho bất kỳ ai muốn đánh nó ra khỏi công viên. Thuốc generic là gì, ưu điểm của thuốc generic là gì, tại sao, ở đâu, tôi nên sử dụng chúng như thế nào? Hãy giữ nó khá cơ bản. Cảm ơn.
Câu trả lời:
Tôi thực sự ghét phải lặp lại chính mình. Tôi ghét gõ cùng một thứ thường xuyên hơn tôi phải làm. Tôi không thích lặp lại mọi thứ nhiều lần với những khác biệt nhỏ.
Thay vì tạo:
class MyObjectList {
MyObject get(int index) {...}
}
class MyOtherObjectList {
MyOtherObject get(int index) {...}
}
class AnotherObjectList {
AnotherObject get(int index) {...}
}
Tôi có thể tạo một lớp có thể tái sử dụng ... (trong trường hợp bạn không muốn sử dụng bộ sưu tập thô vì lý do nào đó)
class MyList<T> {
T get(int index) { ... }
}
Bây giờ tôi hiệu quả hơn gấp 3 lần và tôi chỉ phải duy trì một bản sao. Tại sao bạn KHÔNG muốn duy trì mã ít hơn?
Điều này cũng đúng đối với các lớp không phải là tập hợp như a Callable<T>
hoặc a Reference<T>
phải tương tác với các lớp khác. Bạn có thực sự muốn mở rộng Callable<T>
và Future<T>
mọi lớp liên quan khác để tạo các phiên bản an toàn kiểu không?
Tôi không.
Không cần đánh máy là một trong những lợi thế lớn nhất của Java generic , vì nó sẽ thực hiện kiểm tra kiểu tại thời điểm biên dịch. Điều này sẽ làm giảm khả năng ClassCastException
s có thể được ném trong thời gian chạy và có thể dẫn đến mã mạnh hơn.
Nhưng tôi nghi ngờ rằng bạn hoàn toàn nhận thức được điều đó.
Mỗi khi tôi nhìn vào Generics, nó khiến tôi đau đầu. Tôi thấy phần tốt nhất của Java là sự đơn giản và cú pháp tối thiểu và tổng thể không đơn giản và thêm một lượng lớn cú pháp mới.
Lúc đầu, tôi cũng không thấy lợi ích của thuốc generic. Tôi bắt đầu học Java từ cú pháp 1.4 (mặc dù Java 5 đã ra mắt vào thời điểm đó) và khi tôi gặp phải các khái niệm chung, tôi cảm thấy viết nhiều mã hơn và tôi thực sự không hiểu lợi ích của nó.
Các IDE hiện đại làm cho việc viết mã bằng generic dễ dàng hơn.
Hầu hết các IDE hiện đại, tốt đều đủ thông minh để hỗ trợ viết mã bằng các mã chung, đặc biệt là khi hoàn thành mã.
Đây là một ví dụ về cách tạo Map<String, Integer>
với a HashMap
. Mã tôi sẽ phải nhập là:
Map<String, Integer> m = new HashMap<String, Integer>();
Và thực sự, có rất nhiều thứ để nhập chỉ để tạo ra một cái mới HashMap
. Tuy nhiên, trên thực tế, tôi chỉ phải nhập nhiều điều này trước khi Eclipse biết tôi cần gì:
Map<String, Integer> m = new Ha
Ctrl+Space
Đúng, tôi cần phải chọn HashMap
từ danh sách các ứng cử viên, nhưng về cơ bản IDE biết phải thêm những gì, bao gồm cả các loại chung chung. Với các công cụ phù hợp, việc sử dụng generic không quá tệ.
Ngoài ra, vì các kiểu đã biết nên khi truy xuất các phần tử từ tập hợp chung, IDE sẽ hoạt động như thể đối tượng đó đã là một đối tượng thuộc kiểu được khai báo của nó - không cần ép kiểu để IDE biết kiểu của đối tượng. Là.
Lợi thế chính của generics đến từ cách nó hoạt động tốt với các tính năng Java 5 mới. Đây là một ví dụ về việc chuyển các số nguyên vào a Set
và tính tổng của nó:
Set<Integer> set = new HashSet<Integer>();
set.add(10);
set.add(42);
int total = 0;
for (int i : set) {
total += i;
}
Trong đoạn mã đó, có ba tính năng mới của Java 5:
Đầu tiên, generic và autoboxing các nguyên thủy cho phép các dòng sau:
set.add(10);
set.add(42);
Số nguyên 10
được tự động đóng hộp thành một Integer
với giá trị là 10
. (Và tương tự cho 42
). Sau đó, nó Integer
được ném vào Set
cái được biết là giữ Integer
s. Cố gắng ném vào một String
sẽ gây ra lỗi biên dịch.
Tiếp theo, vòng lặp for-each lấy cả ba điều sau:
for (int i : set) {
total += i;
}
Đầu tiên, các s Set
chứa Integer
được sử dụng trong vòng lặp for-each. Mỗi phần tử được khai báo là một int
và được cho phép khi phần tử Integer
được mở hộp trở lại nguyên thủy int
. Và thực tế là việc mở hộp này xảy ra được biết đến bởi vì generic đã được sử dụng để chỉ định rằng có những thứ được Integer
giữ trong Set
.
Generics có thể là chất kết dính kết hợp các tính năng mới được giới thiệu trong Java 5 và nó chỉ làm cho việc viết mã trở nên đơn giản và an toàn hơn. Và hầu hết thời gian IDE đủ thông minh để giúp bạn đưa ra các đề xuất tốt, vì vậy nói chung, bạn sẽ không phải nhập nhiều hơn.
Và thành thật mà nói, như có thể thấy từ Set
ví dụ, tôi cảm thấy rằng việc sử dụng các tính năng của Java 5 có thể làm cho mã ngắn gọn và mạnh mẽ hơn.
Chỉnh sửa - Một ví dụ không có số liệu chung
Sau đây là minh họa của Set
ví dụ trên mà không cần sử dụng thuốc generic. Có thể, nhưng không hẳn là dễ chịu:
Set set = new HashSet();
set.add(10);
set.add(42);
int total = 0;
for (Object o : set) {
total += (Integer)o;
}
(Lưu ý: Đoạn mã trên sẽ tạo ra cảnh báo chuyển đổi không được kiểm tra tại thời điểm biên dịch.)
Khi sử dụng các bộ sưu tập không phải generics, các kiểu được nhập vào bộ sưu tập là các đối tượng của kiểu Object
. Do đó, trong ví dụ này, a Object
là những gì đang được add
đưa vào tập hợp.
set.add(10);
set.add(42);
Trong các dòng trên, autoboxing đang được phát - int
giá trị nguyên thủy 10
và 42
đang được autoboxed thành Integer
các đối tượng, đang được thêm vào Set
. Tuy nhiên, hãy nhớ rằng, các Integer
đối tượng đang được xử lý dưới dạng Object
s, vì không có thông tin về kiểu để giúp trình biên dịch biết kiểu nào Set
nên mong đợi.
for (Object o : set) {
Đây là phần rất quan trọng. Lý do cho-mỗi vòng lặp làm việc là do Set
thực hiện các Iterable
giao diện, mà trả về một Iterator
với loại thông tin, nếu có. ( Iterator<T>
, đó là.)
Tuy nhiên, vì không có thông tin kiểu, nên Set
sẽ trả về an Iterator
sẽ trả về các giá trị Set
dưới dạng Object
s, và đó là lý do tại sao phần tử được truy xuất trong vòng lặp for-each phải là kiểu Object
.
Bây giờ, khi Object
được truy xuất từ Set
, nó cần được truyền đến một Integer
thủ công để thực hiện thêm:
total += (Integer)o;
Ở đây, một typecast được thực hiện từ an Object
đến an Integer
. Trong trường hợp này, chúng tôi biết điều này sẽ luôn hoạt động, nhưng việc đánh máy thủ công luôn khiến tôi cảm thấy rằng đó là mã mỏng manh có thể bị hỏng nếu một thay đổi nhỏ được thực hiện ở nơi khác. (Tôi cảm thấy rằng mỗi lần đánh máy là một sự ClassCastException
chờ đợi để xảy ra, nhưng tôi lạc đề ...)
Các Integer
hiện đang mở hộp thành int
và được phép thực hiện việc bổ sung vào int
biến total
.
Tôi hy vọng mình có thể minh họa rằng các tính năng mới của Java 5 có thể sử dụng với mã không chung chung, nhưng nó không rõ ràng và dễ hiểu như viết mã bằng mã chung. Và, theo ý kiến của tôi, để tận dụng đầy đủ các tính năng mới trong Java 5, người ta nên xem xét các generic, nếu ít nhất, cho phép kiểm tra thời gian biên dịch để ngăn chặn các typecast không hợp lệ tạo ra các ngoại lệ trong thời gian chạy.
Nếu bạn đã tìm kiếm cơ sở dữ liệu lỗi Java ngay trước 1.5 được phát hành, bạn muốn tìm gấp bảy lần nữa lỗi với NullPointerException
hơn ClassCastException
. Vì vậy, có vẻ như đây không phải là một tính năng tuyệt vời để tìm lỗi, hoặc ít nhất là lỗi vẫn tồn tại sau một thử nghiệm nhỏ.
Đối với tôi, lợi thế lớn của generic là chúng ghi lại thông tin loại quan trọng trong mã . Nếu tôi không muốn thông tin kiểu đó được ghi lại trong mã, thì tôi sẽ sử dụng ngôn ngữ được nhập động hoặc ít nhất là ngôn ngữ có suy luận kiểu ẩn hơn.
Giữ các bộ sưu tập của một đối tượng cho riêng nó không phải là một phong cách tồi (nhưng sau đó, phong cách phổ biến là bỏ qua một cách hiệu quả tính năng đóng gói). Nó phụ thuộc vào những gì bạn đang làm. Việc chuyển các bộ sưu tập đến "thuật toán" sẽ dễ dàng hơn một chút để kiểm tra (tại hoặc trước thời điểm biên dịch) với generic.
Generics trong Java tạo điều kiện cho tính đa hình tham số . Bằng các tham số kiểu, bạn có thể truyền các đối số cho các kiểu. Cũng giống như một phương thức giống như String foo(String s)
mô hình một số hành vi, không chỉ cho một chuỗi cụ thể mà cho bất kỳ chuỗi nào s
, vì vậy một kiểu giống như List<T>
mô hình một số hành vi, không chỉ cho một kiểu cụ thể mà cho bất kỳ kiểu nào . List<T>
nói rằng đối với bất kỳ loại nào T
, có một loại List
có các phần tử là T
s . Vì vậy, List
một thực sự là một phương thức tạo kiểu . Nó nhận một kiểu làm đối số và kết quả là tạo một kiểu khác.
Dưới đây là một vài ví dụ về các loại chung mà tôi sử dụng hàng ngày. Đầu tiên, một giao diện chung rất hữu ích:
public interface F<A, B> {
public B f(A a);
}
Giao diện này cho biết rằng đối với một số loại có hai kiểu, A
và B
có một hàm (được gọi f
) nhận một A
và trả về a B
. Khi bạn triển khai giao diện này, A
và B
có thể là bất kỳ kiểu nào bạn muốn, miễn là bạn cung cấp một hàm f
lấy cái trước và trả về cái sau. Đây là một ví dụ về triển khai giao diện:
F<Integer, String> intToString = new F<Integer, String>() {
public String f(int i) {
return String.valueOf(i);
}
}
Trước khi có generic, tính đa hình đã đạt được bằng cách phân lớp con sử dụng extends
từ khóa. Với generic, chúng ta thực sự có thể loại bỏ phân lớp con và thay vào đó sử dụng tính đa hình tham số. Ví dụ: hãy xem xét một lớp được tham số hóa (chung chung) được sử dụng để tính toán mã băm cho bất kỳ loại nào. Thay vì ghi đè Object.hashCode (), chúng ta sẽ sử dụng một lớp chung như thế này:
public final class Hash<A> {
private final F<A, Integer> hashFunction;
public Hash(final F<A, Integer> f) {
this.hashFunction = f;
}
public int hash(A a) {
return hashFunction.f(a);
}
}
Điều này linh hoạt hơn nhiều so với việc sử dụng kế thừa, bởi vì chúng ta có thể tiếp tục với chủ đề của việc sử dụng bố cục và đa hình tham số mà không khóa các cấu trúc phân cấp giòn.
Mặc dù vậy, tổng thể của Java không hoàn hảo. Bạn có thể trừu tượng hóa trên các kiểu, nhưng bạn không thể trừu tượng hóa các hàm tạo kiểu. Nghĩa là, bạn có thể nói "cho bất kỳ kiểu T nào", nhưng bạn không thể nói "cho bất kỳ kiểu T nào nhận tham số kiểu A".
Tôi đã viết một bài báo về những giới hạn này của Java generic, tại đây.
Một chiến thắng lớn với generic là chúng cho phép bạn tránh phân loại con. Phân lớp con có xu hướng dẫn đến hệ thống phân cấp lớp giòn mà khó mở rộng và các lớp khó hiểu riêng lẻ nếu không nhìn vào toàn bộ hệ thống phân cấp.
Wereas trước Generics bạn có thể có các lớp học như Widget
mở rộng FooWidget
, BarWidget
và BazWidget
, với Generics bạn có thể có một lớp generic đơn Widget<A>
mà phải mất một Foo
, Bar
hoặc Baz
trong constructor của nó để cung cấp cho bạn Widget<Foo>
, Widget<Bar>
và Widget<Baz>
.
Generics tránh hiệu suất đánh quyền anh và unboxing. Về cơ bản, hãy xem ArrayList và List <T>. Cả hai đều làm những điều cốt lõi giống nhau, nhưng List <T> sẽ nhanh hơn rất nhiều vì bạn không phải đóng hộp đến / từ đối tượng.
Lợi ích tốt nhất đối với Generics là sử dụng lại mã. Giả sử rằng bạn có rất nhiều đối tượng kinh doanh và bạn sẽ viết RẤT giống mã cho mỗi đối tượng để thực hiện các hành động giống nhau. (IE Linq tới các phép toán SQL).
Với generics, bạn có thể tạo một lớp có thể hoạt động với bất kỳ kiểu nào kế thừa từ một lớp cơ sở nhất định hoặc triển khai một giao diện nhất định như sau:
public interface IEntity
{
}
public class Employee : IEntity
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int EmployeeID { get; set; }
}
public class Company : IEntity
{
public string Name { get; set; }
public string TaxID { get; set }
}
public class DataService<ENTITY, DATACONTEXT>
where ENTITY : class, IEntity, new()
where DATACONTEXT : DataContext, new()
{
public void Create(List<ENTITY> entities)
{
using (DATACONTEXT db = new DATACONTEXT())
{
Table<ENTITY> table = db.GetTable<ENTITY>();
foreach (ENTITY entity in entities)
table.InsertOnSubmit (entity);
db.SubmitChanges();
}
}
}
public class MyTest
{
public void DoSomething()
{
var dataService = new DataService<Employee, MyDataContext>();
dataService.Create(new Employee { FirstName = "Bob", LastName = "Smith", EmployeeID = 5 });
var otherDataService = new DataService<Company, MyDataContext>();
otherDataService.Create(new Company { Name = "ACME", TaxID = "123-111-2233" });
}
}
Lưu ý việc sử dụng lại cùng một dịch vụ với các Loại khác nhau trong phương thức DoSomething ở trên. Quả thật là tao nhã!
Có nhiều lý do tuyệt vời khác để sử dụng generic cho công việc của bạn, đây là lý do yêu thích của tôi.
Tôi chỉ thích chúng vì chúng cung cấp cho bạn một cách nhanh chóng để xác định một loại tùy chỉnh (vì tôi vẫn sử dụng chúng).
Vì vậy, ví dụ: thay vì xác định một cấu trúc bao gồm một chuỗi và một số nguyên, và sau đó phải triển khai toàn bộ tập hợp các đối tượng và phương thức về cách truy cập một mảng của các cấu trúc đó, v.v., bạn có thể tạo một Từ điển.
Dictionary<int, string> dictionary = new Dictionary<int, string>();
Và trình biên dịch / IDE thực hiện phần còn lại của công việc nặng nhọc. Đặc biệt, một Từ điển cho phép bạn sử dụng kiểu đầu tiên làm khóa (không có giá trị lặp lại).
Các bộ sưu tập đã đánh máy - ngay cả khi bạn không muốn sử dụng chúng, bạn có thể phải xử lý chúng từ các thư viện khác, các nguồn khác.
Nhập chung trong tạo lớp:
public class Foo <T> {public T get () ...
Tránh casting - tôi luôn không thích những thứ như
new Comparator {public int so sánhTo (Object o) {if (o instanceof classIcareAbout) ...
Về cơ bản bạn đang kiểm tra một điều kiện chỉ nên tồn tại vì giao diện được thể hiện dưới dạng các đối tượng.
Phản ứng ban đầu của tôi đối với thuốc chung tương tự như của bạn - "quá lộn xộn, quá phức tạp". Kinh nghiệm của tôi là sau khi sử dụng chúng một chút, bạn sẽ quen với chúng, và mã mà không có chúng cảm thấy ít được chỉ định rõ ràng hơn và ít thoải mái hơn. Bên cạnh đó, phần còn lại của thế giới java sử dụng chúng nên cuối cùng bạn sẽ phải làm quen với chương trình, phải không?
Để đưa ra một ví dụ điển hình. Hãy tưởng tượng bạn có một lớp học tên là Foo
public class Foo
{
public string Bar() { return "Bar"; }
}
Ví dụ 1 Bây giờ bạn muốn có một bộ sưu tập các đối tượng Foo. Bạn có hai tùy chọn, LIst hoặc ArrayList, cả hai đều hoạt động theo cách tương tự.
Arraylist al = new ArrayList();
List<Foo> fl = new List<Foo>();
//code to add Foos
al.Add(new Foo());
f1.Add(new Foo());
Trong đoạn mã trên, nếu tôi cố gắng thêm một lớp FireTruck thay vì Foo, ArrayList sẽ thêm nó, nhưng Danh sách chung của Foo sẽ khiến một ngoại lệ được ném ra.
Ví dụ hai.
Bây giờ bạn có hai danh sách mảng và bạn muốn gọi hàm Bar () trên mỗi danh sách. Vì hte ArrayList chứa đầy các Đối tượng, bạn phải truyền chúng trước khi có thể gọi thanh. Nhưng vì Danh sách chung của Foo chỉ có thể chứa Foos, bạn có thể gọi trực tiếp Bar () trên các Foo đó.
foreach(object o in al)
{
Foo f = (Foo)o;
f.Bar();
}
foreach(Foo f in fl)
{
f.Bar();
}
Bạn chưa bao giờ viết một phương thức (hoặc một lớp) trong đó khái niệm chính của phương thức / lớp không bị ràng buộc chặt chẽ với một kiểu dữ liệu cụ thể của các tham số / biến phiên bản (hãy nghĩ đến danh sách liên kết, hàm max / min, tìm kiếm nhị phân , Vân vân.).
Bạn chưa bao giờ ước mình có thể sử dụng lại thuật toán / mã mà không cần sử dụng lại cắt-n-dán hoặc ảnh hưởng đến việc nhập mạnh (ví dụ: tôi muốn một List
chuỗi, không phải một List
trong những thứ tôi hy vọng là chuỗi!)?
Đó là lý do tại sao bạn nên muốn sử dụng thuốc generic (hoặc một cái gì đó tốt hơn).
Đừng quên rằng generic không chỉ được sử dụng bởi các lớp, chúng cũng có thể được sử dụng bởi các phương thức. Ví dụ: lấy đoạn mã sau:
private <T extends Throwable> T logAndReturn(T t) {
logThrowable(t); // some logging method that takes a Throwable
return t;
}
Nó đơn giản, nhưng có thể được sử dụng rất trang nhã. Điều thú vị là phương thức này trả về bất cứ thứ gì mà nó đã được đưa ra. Điều này giúp ích khi bạn đang xử lý các ngoại lệ cần được trả lại cho người gọi:
...
} catch (MyException e) {
throw logAndReturn(e);
}
Vấn đề là bạn không bị mất kiểu bằng cách chuyển nó qua một phương thức. Bạn có thể ném đúng loại ngoại lệ thay vì chỉThrowable
, đó sẽ là tất cả những gì bạn có thể làm mà không có generic.
Đây chỉ là một ví dụ đơn giản về một lần sử dụng cho các phương pháp chung. Có một số điều khác bạn có thể làm với các phương pháp chung chung. Điều thú vị nhất, theo ý kiến của tôi, là kiểu suy luận bằng generic. Lấy ví dụ sau (lấy từ Phiên bản Java thứ 2 hiệu quả của Josh Bloch):
...
Map<String, Integer> myMap = createHashMap();
...
public <K, V> Map<K, V> createHashMap() {
return new HashMap<K, V>();
}
Điều này không làm được nhiều, nhưng nó cắt giảm một số lộn xộn khi các kiểu chung dài (hoặc lồng nhau; tức là Map<String, List<String>>
).
Throwable
từ bên trong thân phương thức có các ngoại lệ được khai báo cụ thể. Cách thay thế là viết các phương thức riêng biệt để trả về từng loại ngoại lệ hoặc tự thực hiện ép kiểu với một phương thức không chung chung trả về a Throwable
. Cái trước quá dài dòng và khá vô dụng và cái sau sẽ không nhận được bất kỳ sự trợ giúp nào từ trình biên dịch. Bằng cách sử dụng generic, trình biên dịch sẽ tự động chèn đúng kiểu cho bạn. Vì vậy, câu hỏi đặt ra là: điều đó có xứng đáng với sự phức tạp không?
Ưu điểm chính, như Mitchel chỉ ra, là gõ mạnh mà không cần xác định nhiều lớp.
Bằng cách này, bạn có thể làm những việc như:
List<SomeCustomClass> blah = new List<SomeCustomClass>();
blah[0].SomeCustomFunction();
Nếu không có generic, bạn sẽ phải truyền blah [0] đến đúng loại để truy cập các chức năng của nó.
kiểu jvm nào cũng vậy ... nó hoàn toàn tạo ra mã coi kiểu chung là "Đối tượng" và tạo ra các phôi theo kiểu khởi tạo mong muốn. Các generic Java chỉ là đường cú pháp.
Tôi biết đây là một câu hỏi C #, nhưng chung chung các ngôn ngữ cũng được sử dụng trong các ngôn ngữ khác và việc sử dụng / mục tiêu của chúng khá giống nhau.
Bộ sưu tập Java sử dụng generic kể từ Java 1.5. Vì vậy, một nơi tốt để sử dụng chúng là khi bạn đang tạo đối tượng giống bộ sưu tập của riêng mình.
Một ví dụ mà tôi thấy hầu như ở khắp mọi nơi là lớp Pair, chứa hai đối tượng, nhưng cần phải xử lý các đối tượng đó theo cách chung.
class Pair<F, S> {
public final F first;
public final S second;
public Pair(F f, S s)
{
first = f;
second = s;
}
}
Bất cứ khi nào bạn sử dụng lớp Pair này, bạn có thể chỉ định loại đối tượng nào bạn muốn nó xử lý và mọi vấn đề về kiểu ép kiểu sẽ hiển thị tại thời điểm biên dịch, thay vì thời gian chạy.
Generics cũng có thể có giới hạn của chúng được xác định với các từ khóa 'siêu' và 'mở rộng'. Ví dụ: nếu bạn muốn xử lý một kiểu chung nhưng bạn muốn đảm bảo rằng nó mở rộng một lớp có tên Foo (có phương thức setTitle):
public class FooManager <F extends Foo>{
public void setTitle(F foo, String title) {
foo.setTitle(title);
}
}
Mặc dù tự nó không thú vị lắm, nhưng thật hữu ích khi biết rằng bất cứ khi nào bạn xử lý FooManager, bạn biết rằng nó sẽ xử lý các loại MyClass và MyClass mở rộng Foo.
Từ tài liệu Sun Java, để trả lời "tại sao tôi nên sử dụng generic?":
"Generics cung cấp một cách để bạn giao tiếp loại bộ sưu tập với trình biên dịch để có thể kiểm tra nó. Sau khi trình biên dịch biết loại phần tử của bộ sưu tập, trình biên dịch có thể kiểm tra xem bạn đã sử dụng bộ sưu tập một cách nhất quán chưa và có thể chèn nhập đúng các giá trị được lấy ra khỏi bộ sưu tập ... Mã sử dụng generic rõ ràng và an toàn hơn .... trình biên dịch có thể xác minh tại thời điểm biên dịch rằng các ràng buộc kiểu không bị vi phạm tại thời điểm chạy [nhấn mạnh của tôi]. Bởi vì chương trình biên dịch mà không có cảnh báo, chúng tôi có thể khẳng định chắc chắn rằng nó sẽ không ném ClassCastException vào thời gian chạy. Hiệu quả thực sự của việc sử dụng generic, đặc biệt là trong các chương trình lớn, là cải thiện khả năng đọc và độ mạnh mẽ . [nhấn mạnh của tôi] "
Generics cho phép bạn tạo các đối tượng được gõ mạnh, nhưng bạn không phải xác định kiểu cụ thể. Tôi nghĩ ví dụ hữu ích nhất là Danh sách và các lớp tương tự.
Sử dụng danh sách chung, bạn có thể có một Danh sách Danh sách bất cứ thứ gì bạn muốn và bạn luôn có thể tham khảo cách gõ mạnh, bạn không cần phải chuyển đổi hoặc bất cứ điều gì giống như bạn làm với Mảng hoặc Danh sách chuẩn.
Generics cho phép bạn sử dụng cách gõ mạnh cho các đối tượng và cấu trúc dữ liệu có thể chứa bất kỳ đối tượng nào. Nó cũng giúp loại bỏ các kiểu gõ tẻ nhạt và tốn kém khi lấy các đối tượng từ các cấu trúc chung (quyền anh / mở hộp).
Một ví dụ sử dụng cả hai là danh sách liên kết. Một lớp danh sách liên kết sẽ có ích gì nếu nó chỉ có thể sử dụng đối tượng Foo? Để triển khai danh sách liên kết có thể xử lý bất kỳ loại đối tượng nào, danh sách được liên kết và các nút trong lớp bên trong nút giả định phải là chung nếu bạn muốn danh sách chỉ chứa một loại đối tượng.
Một lợi thế khác của việc sử dụng Generics (đặc biệt với Bộ sưu tập / Danh sách) là bạn có thể kiểm tra loại thời gian biên dịch. Điều này thực sự hữu ích khi sử dụng Danh sách Chung thay vì Danh sách Đối tượng.
Lý do duy nhất là họ cung cấp an toàn cho Loại
List<Customer> custCollection = new List<Customer>;
như trái ngược với,
object[] custCollection = new object[] { cust1, cust2 };
như một ví dụ đơn giản.
Tóm lại, generic cho phép bạn chỉ định chính xác hơn những gì bạn định làm (gõ mạnh hơn).
Điều này có một số lợi ích cho bạn:
Bởi vì trình biên dịch biết nhiều hơn về những gì bạn muốn làm, nó cho phép bạn bỏ qua rất nhiều kiểu ép kiểu vì nó đã biết rằng kiểu sẽ tương thích.
Điều này cũng giúp bạn có phản hồi sớm hơn về những điểm chính xác của chương trình của bạn. Những thứ mà trước đây sẽ không thành công trong thời gian chạy (ví dụ: vì một đối tượng không thể được truyền theo kiểu mong muốn), bây giờ bị lỗi trong thời gian biên dịch và bạn có thể sửa lỗi trước khi bộ phận kiểm tra của bạn gửi báo cáo lỗi kỹ thuật số.
Trình biên dịch có thể thực hiện nhiều tối ưu hóa hơn, như tránh đấm bốc, v.v.
Một số điều cần thêm / mở rộng (nói theo quan điểm .NET):
Các kiểu chung cho phép bạn tạo các lớp và giao diện dựa trên vai trò. Điều này đã được nói trong các thuật ngữ cơ bản hơn, nhưng tôi thấy bạn bắt đầu thiết kế mã của mình với các lớp được triển khai theo kiểu bất khả tri - dẫn đến mã có thể tái sử dụng cao.
Các lập luận chung chung về các phương thức có thể làm điều tương tự, nhưng chúng cũng giúp áp dụng nguyên tắc "Tell Don't Ask" để ép kiểu, tức là "đưa cho tôi thứ tôi muốn, và nếu bạn không thể, bạn cho tôi biết tại sao".
Tôi sử dụng chúng chẳng hạn trong một GenericDao được triển khai với SpringORM và Hibernate trông như thế này
public abstract class GenericDaoHibernateImpl<T>
extends HibernateDaoSupport {
private Class<T> type;
public GenericDaoHibernateImpl(Class<T> clazz) {
type = clazz;
}
public void update(T object) {
getHibernateTemplate().update(object);
}
@SuppressWarnings("unchecked")
public Integer count() {
return ((Integer) getHibernateTemplate().execute(
new HibernateCallback() {
public Object doInHibernate(Session session) {
// Code in Hibernate for getting the count
}
}));
}
.
.
.
}
Bằng cách sử dụng generics, việc triển khai DAO này của tôi buộc nhà phát triển phải chuyển chúng chỉ những thực thể mà chúng được thiết kế bằng cách chỉ phân lớp GenericDao
public class UserDaoHibernateImpl extends GenericDaoHibernateImpl<User> {
public UserDaoHibernateImpl() {
super(User.class); // This is for giving Hibernate a .class
// work with, as generics disappear at runtime
}
// Entity specific methods here
}
Khung công tác nhỏ của tôi mạnh mẽ hơn (có những thứ như lọc, tải lười biếng, tìm kiếm). Tôi chỉ đơn giản hóa ở đây để cung cấp cho bạn một ví dụ
Tôi cũng như Steve và bạn, lúc đầu nói "Quá lộn xộn và phức tạp" nhưng bây giờ tôi thấy ưu điểm của nó
Các lợi ích rõ ràng như "an toàn kiểu" và "không truyền" đã được đề cập nên có lẽ tôi có thể nói về một số "lợi ích" khác mà tôi hy vọng nó sẽ hữu ích.
Trước hết, generics là một khái niệm không phụ thuộc vào ngôn ngữ và IMO, nó có thể có ý nghĩa hơn nếu bạn nghĩ về tính đa hình thông thường (thời gian chạy) cùng một lúc.
Ví dụ, tính đa hình như chúng ta biết từ thiết kế hướng đối tượng có khái niệm thời gian chạy trong đó đối tượng người gọi được tìm ra trong thời gian chạy khi thực thi chương trình và phương thức liên quan được gọi tương ứng tùy thuộc vào loại thời gian chạy. Trong generic, ý tưởng hơi giống nhau nhưng mọi thứ xảy ra tại thời điểm biên dịch. Điều đó có nghĩa là gì và bạn sử dụng nó như thế nào?
(Hãy gắn bó với các phương thức chung để giữ cho nó nhỏ gọn) Điều đó có nghĩa là bạn vẫn có thể có cùng một phương thức trên các lớp riêng biệt (giống như bạn đã làm trước đây trong các lớp đa hình) nhưng lần này chúng được trình biên dịch tự động tạo tùy thuộc vào các kiểu được thiết lập tại thời điểm biên dịch. Bạn tham số các phương thức của mình theo kiểu bạn cung cấp tại thời điểm biên dịch. Vì vậy, thay vì viết các phương pháp từ đầu cho mọi loại bạn có như khi bạn làm trong đa hình thời gian chạy (ghi đè phương thức), bạn để các trình biên dịch thực hiện công việc trong quá trình biên dịch. Điều này có một lợi thế rõ ràng vì bạn không cần phải suy ra tất cả các kiểu có thể được sử dụng trong hệ thống của bạn, điều này làm cho nó có thể mở rộng hơn nhiều mà không cần thay đổi mã.
Các lớp học hoạt động theo cùng một cách. Bạn tham số kiểu và mã được tạo bởi trình biên dịch.
Một khi bạn có ý tưởng về "thời gian biên dịch", bạn có thể sử dụng các kiểu "giới hạn" và hạn chế những gì có thể được truyền dưới dạng kiểu tham số thông qua các lớp / phương thức. Vì vậy, bạn có thể kiểm soát những gì sẽ được thông qua, đó là một điều mạnh mẽ, đặc biệt là bạn có một khuôn khổ đang bị người khác sử dụng.
public interface Foo<T extends MyObject> extends Hoo<T>{
...
}
Không ai có thể đặt sth ngoài MyObject bây giờ.
Ngoài ra, bạn có thể "thực thi" các ràng buộc kiểu đối với các đối số phương thức của mình, nghĩa là bạn có thể đảm bảo rằng cả hai đối số phương thức của bạn sẽ phụ thuộc vào cùng một kiểu.
public <T extends MyObject> foo(T t1, T t2){
...
}
Hy vọng tất cả những điều này có ý nghĩa.
Tôi đã từng thuyết trình về chủ đề này. Bạn có thể tìm thấy các trang trình bày, mã và bản ghi âm của tôi tại http://www.adventuresinsoftware.com/generics/ .
Sử dụng generic cho các bộ sưu tập rất đơn giản và gọn gàng. Ngay cả khi bạn chơi nó ở mọi nơi khác, thu được từ các bộ sưu tập là một chiến thắng đối với tôi.
List<Stuff> stuffList = getStuff();
for(Stuff stuff : stuffList) {
stuff.do();
}
vs
List stuffList = getStuff();
Iterator i = stuffList.iterator();
while(i.hasNext()) {
Stuff stuff = (Stuff)i.next();
stuff.do();
}
hoặc là
List stuffList = getStuff();
for(int i = 0; i < stuffList.size(); i++) {
Stuff stuff = (Stuff)stuffList.get(i);
stuff.do();
}
Chỉ điều đó thôi cũng đáng với "chi phí" biên của thuốc chung và bạn không cần phải là một Guru chung chung để sử dụng cái này và nhận được giá trị.
Generics cũng cung cấp cho bạn khả năng tạo nhiều đối tượng / phương thức có thể tái sử dụng hơn trong khi vẫn cung cấp hỗ trợ loại cụ thể. Bạn cũng đạt được rất nhiều hiệu suất trong một số trường hợp. Tôi không biết thông số kỹ thuật đầy đủ về Java Generics, nhưng trong .NET, tôi có thể chỉ định các ràng buộc trên tham số Kiểu, như Triển khai giao diện, Khối mã lệnh và Khởi tạo.
Cho phép lập trình viên triển khai các thuật toán chung - Bằng cách sử dụng các thuật toán chung, các lập trình viên có thể triển khai các thuật toán chung hoạt động trên các tập hợp thuộc các kiểu khác nhau, có thể được tùy chỉnh, an toàn về kiểu và dễ đọc hơn.
Kiểm tra kiểu mạnh hơn tại thời điểm biên dịch - Trình biên dịch Java áp dụng kiểm tra kiểu mạnh cho mã chung và đưa ra lỗi nếu mã vi phạm an toàn kiểu. Sửa lỗi thời gian biên dịch dễ hơn sửa lỗi thời gian chạy, có thể khó tìm.
Loại bỏ phôi.