为什么引入泛型

bug是编程的一部分,我们仅仅能尽自己最大的能力降低出现bug的几率,可是谁也不能保证自己写出的程序不出现不论什么问题。

错误可分为两种:编译时错误与执行时错误。编译时错误在编译时能够发现并排除。而执行时错误具有非常大的不确定性,在程序执行时才干发现。造成的后果可能是灾难性的。

使用泛型能够使错误在编译时被探測到,从而添加程序的健壮性。

来看一个样例:

public class Box{
private Object object; public void set(Object object) {
this.object= object;
}
public Object get() {
return object;
}
}

依照声明。其中的set()方法能够接受不论什么java对象作为參数(不论什么对象都是Object的子类),假如在某个地方使用该类,set()方法预期的输入对象为Integer类型,可是实际输入的却是String类型,就会抛出一个执行时错误,这个错误在编译阶段是无法检測的。比如:

Box box = new Box;
box.set("abc");
Integer a =(Integer)box.get(); //编译时不会报错,可是执行时会报ClassCastException

运用泛型改造上面的代码:

public class Box<T>{
private T t; public void set(T t) {
this.t= t;
}
public T get() {
return t;
}
}

当我们使用该类时会指定T的详细类型,该类型參数能够是类、接口、数组等。可是不能是基本类型。

比方:

Box<Integer> box = new Box<Integer>; //指定了类型类型为Integer
//box.set("abc"); 该句在编译时就会报错
box.set(new Integer(2));
Integer a = box.get(); //不用转换类型

能够看到,使用泛型还免除了转换操作。

在引入泛型机制之前,要在方法中支持多个数据类型。须要对方法进行重载,在引入范型后,能够更简洁地解决此问题,更进一步能够定义多个參数以及返回值之间的关系。

比如

public void write(Integer i, Integer[] ia);
public void write(Double d, Double[] da);
public void write(Long l, Long[] la);

的范型版本号为

public <T> void write(T t, T[] ta);

整体来说。泛型机制能够在定义类、接口、方法时把“类型”当做參数使用,有点相似于方法声明中的形式參数,如此我们就能通过不同的输入參数来实现程序的重用。不同的是,形式參数的输入是值,而泛型參数的输入是类型。

命名规则

类型參数的命名有一套默认规则,为了提高代码的维护性和可读性,强烈建议遵循这些规则。JDK中。随处可见这些命名规则的应用。

E - Element (通常代表集合类中的元素)

K - Key

N - Number

T - Type

V - Value

S,U,V etc. – 第二个,第三个,第四个类型參数……

注意。父类定义的类型參数不能被子类继承。

也能够同一时候声明多个类型变量,用逗号切割。比如:

public interface Pair<K, V> {
public K getKey();
public V getValue();
} public class OrderedPair<K, V> implements Pair<K, V> { private K key;
private V value; public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
} public K getKey() { return key; }
public V getValue() { return value; }
}

以下的两行代码创建了OrderedPair对象的两个实例。

Pair<String,Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
Pair<String,String> p2 = new OrderedPair<String, String>("hello", "world"); //也能够将new后面的类型參数省略。简写为:
//Pair<String,Integer> p1 = new OrderedPair<>("Even", 8); //也能够在尖括号内使用带有类型变量的类型变量。比如:
OrderedPair<String,Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));

泛型是JDK 5.0之后才引入的,为了兼容性,同意不指定泛型參数,可是如此一来,编译器就无法进行类型检查,在编程时,最好明白指定泛型參数。

相同,在方法中也可是使用泛型參数,而且该參数的使用范围仅限于方法体内。

比如:

public class Util {
//该方法用于比較两个Pair对象是否相等。
//泛型參数必须写在方法返回类型boolean之前
public static <K, V> boolean compare(Pair<K,V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey())&&
p1.getValue().equals(p2.getValue());
}
} Pair<Integer,String> p1 = new Pair<>(1, "apple");
Pair<Integer,String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);
//实际上。编译器能够通过Pair其中的类型来判断compare须要使用的类型,所以能够简写为:
// boolean same= Util. compare(p1, p2);

有时候我们想让类型參数限定在某个范围之内,就须要用到extendskeyword(extends后面能够跟一个接口。这里的extends既能够表示继承了某个类,也能够表示实现了某个接口),比如。我们想让參数是数字类型:

class Box<T extends Number>{  //类型參数限定为Number的子类

      private T t;

      public Box(T t){
this.t = t;
}
public void print(){
System.out.println(t.getClass().getName());
} public static void main(String[] args) { Box<Integer> box1 = new Box<Integer>(new Integer(2));
box1.print(); //打印结果:java.lang.Integer
Box<Double> box2 = new Box<Double>(new Double(1.2));
box2.print(); //打印结果:java.lang.Double Box<String> box2 = new Box<String>(new String("abc")); //报错,由于String类型不是Number的子类
box2.print();
} }

假设加入多个限定,能够用“&”连接起来,可是由于java是单继承,多个限定中最多仅仅能有一个类,而且必须放在第一个位置。比如:

class Box<T extends Number & Cloneable & Comparable >{
//该类型必须为Number的子类而且实现了Cloneable接口和Comparable接口。
……
}

泛型类的继承

java是面向对象的高级语言。在一个接受A类參数的地方传入一个A的子类是同意的,比如:

Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject =someInteger; // 由于Integer是Object的子类

这样的特性相同适用类型參数,比如:

Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // Integer是Number的子类
box.add(new Double(10.1)); // Double相同是Number的子类

可是,有一种情况非常easy引起混淆,比如:

//该方法接受的參数类型为Box<Number>
public void boxTest(Box<Number> n) {
……
}
//以下两种调用都会报错
boxTest(Box<Integer>);
boxTest(Box<Double>);

虽然Integer和Double都是Number的子类,可是Box<Integer>与Box<Double>并非Box<Number>的子类,不存在继承关系。Box<Integer>与Box<Double>的共同父类是Object。

以JDK中的集合类为例,ArrayList<E>实现了 List<E>接口。List<E>接口继承了 Collection<E>接口,所以,ArrayList<String>是List<String>的子类。而非List<Integer>的子类。三者的继承关系例如以下:

类型判断(Type Inference)

先来看一个样例:

public class Demo{

      static <T> T pick(T a1, T a2) {
return a2;
} }

静态方法pick()在三个地方使用了泛型,分别限定了两个输入參数的类型与返回类型。

调用该方法的代码例如以下:

Integer ret =Demo.<Integer> pick(new Integer(1), new Integer(2));
//前文已经提到,上面的代码能够简写为:
Integer ret =Demo. pick(new Integer(1), new Integer(2));

由于java编译器会依据方法内的參数类型判断出该方法返回的类型应该为Integer,这样的机制称为类型判断

那么问题来了,假如两个输入參数为不同的类型。应该返回什么类型呢?

比如:

pick("d", new ArrayList<String>());

第一个參数为String类型,第二个參数为ArrayList类型,java编译器就会依据这两个參数类型来判断。尽量使返回类型为最明白的一种。本例中,String与ArrayList都实现了相同的接口——Serializable,当然,他们也是Object的子类,Serializable类型显然比Object类型更加明白,由于它的范围更小更细分。所以终于的返回类型应该为Serializable:

Serializable s =pick("d", new ArrayList<String>());

在泛型类实例化的时候相同能够利用这样的机制简化代码,须要注意的是,尖括号“<>”在此时是不能省略的。比如:

Map<String,List<String>> myMap = new HashMap<>();
//编译器能判断出后面的类型,所以能够简化为:
Map<String,List<String>> myMap = new HashMap<>();
//可是,不能简化为:
Map<String,List<String>> myMap =new HashMap();
//由于HashMap()是HashMap原始类型(Raw Type)的构造函数,而非HashMap<String,List<String>>的构造函数,假设不加“<>”编译器不会进行类型检查

通配符

上文中我们提到过一个样例:

public void boxTest(Box<Number> n){
……
}

该方法仅仅能接受Box<Number>这一种类型的參数,当我们输入一个Box<Double>或者Box<Integer>时会报错,虽然Integer与Double是Number的子类。可是假设我们希望该方法能够接受Number以及它的不论什么子类,该怎么办呢?

这时候就要用到通配符了,改写例如以下:

public void boxTest(Box<?

extends Number> n){
……
}

“? extends Number”就代表能够接受Number以及它的子类作为參数。

这样的声明方式被称为上限通配符(upper bounded wildcard)。

相反地。假设我们希望该方法能够接受Integer,Number以及Object类型的參数怎么办呢?应该使用下限通配符(lower bounded wildcard):

public void boxTest(Box<? super Integer> n){
……
}

“?

super Integer”代表能够接受Integer以及它的父类作为參数。

假设类型參数中既没有extends keyword,也没有superkeyword。仅仅有一个?,代表无限定通配符(Unbounded Wildcards)。

通常在两种情况下会使用无限定通配符:

(1)假设正在编写一个方法,能够使用Object类中提供的功能来实现

(2)代码实现的功能与类型參数无关,比方List.clear()与List.size()方法。还有常常使用的Class<?

>方法,事实上现的功能都与类型參数无关。

来看一个样例:

public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + "");
System.out.println();
}

该方法仅仅能接受List<Object>型的參数。不接受其它不论什么类型的參数。

可是。该方法实现的功能与List之中參数类型没有关系,所以我们希望它能够接受包括不论什么类型的List參数。代码修改例如以下:

public static void printList(List<?> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}

须要特别注意的是,List<?>与List<Object>并不相同。不管A是什么类型,List<A>是List<?

>的子类,可是。List<A>不是List<Object>的子类。

比如:

List<Number> lb = new ArrayList<>();

List<Integer> la = lb;   // 会报编译错误。虽然Integer是Number的子类,可是List<Integer>不是List<Number>的子类

List<Integer>与List<Number>的关系例如以下:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />

所以,以下的代码是正确的:

List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // 不会报错。 List<? extends Integer> 是 List<? extends Number>的子类

以下这张图介绍了上限通配符、下限通配符、无限定通配符之间的关系:

编译器能够通过类型判断机制来决定通配符的类型,这样的情况被称为通配符捕获。大多时候我们不必操心通配符捕获,除非编译器报出了包括“capture of”的错误。

比如:

public class WildcardError {

    void foo(List<?

> i) {
i.set(0, i.get(0)); //会报编译错误
}
}

上例中,调用List.set(int,E)方法的时候。编译器无法判断i.get(0)是什么类型。就会报错。

我们能够借助一个私有的能够捕获通配符的helper方法来解决这样的错误:

public class WildcardFixed {

    void foo(List<?> i) {
fooHelper(i);
} // 该方法能够确保编译器通过通配符捕获来判断出參数类型
private <T> void fooHelper(List<T> l) {
l.set(0, l.get(0));
} }

依照约定俗成的习惯,helper方法的命名方法为“原始方法”+“helper”,上例中。原始方法为“foo”,所以命名为“fooHelper”

关于什么时候该使用上限通配符。什么时候该使用下限通配符,应该遵循一下几项指导规则。

首先将变量分为in-变量out-变量:in-变量持有为当前代码服务的数据,out-变量持有其它地方须要使用的数据。比如copy(src, dest)方法实现了从src源头将数据拷贝到dest目的地的功能,那么src就是in-变量。而dest就是out-变量。当然,在一些情况下。一个变量可能既是in-变量也是out-变量。

(1)in-变量使用上限通配符。

(2)out-变量使用下限通配符;

(3)当in-变量能够被Object类中的方法訪问时。使用无限定通配符;

(4)一个变量既是in-变量也是out-变量时,不使用通配符

注意,上面的规则不适用于方法的返回类型。


类型擦除(Type Erasure)

java编译器在处理泛型的时候,会做以下几件事:

(1)将没有限定的类型參数用Object替换,保证class文件里仅仅含有正常的类、接口与方法。

(2)在必要的时候进行类型转换,保证类型安全;

(3)在泛型的继承上使用桥接方法(bridge methods)保持多态性。

这类操作被称为类型擦除

比如:

public class Node<T> {

    private T data;
private Node<T> next; public Node(T data, Node<T> next) }
this.data = data;
this.next = next;
} public T getData() { return data; }
// ...
}

该类中的T没有被extends或者super限定。会被编译器替换成Object:

public class Node {

    private Object data;
private Node next; public Node(Object data, Node next) {
this.data = data;
this.next = next;
} public Object getData() { return data; }
// ...
}

假设T加了限定,编译器会将它替换成合适的类型:

public class Node<T extends Comparable<T>> {

    private T data;
private Node<T> next; public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
} public T getData() { return data; }
// ...
}

改造成:

public class Node {

    private Comparable data;
private Node next; public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
} public Comparable getData() { return data;}
//...
}

方法中的类型擦除与之相似。

有时候类型擦除会产生一些我们预想不到的情况,以下通过一个样例来分析它是怎样产生的。

public class Node<T> {

    public T data;

    public Node(T data) { this.data = data; }

    public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
} public class MyNode extends Node<Integer>{
public MyNode(Integer data) { super(data);} public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}

上面的代码定义了两个类,MyNode类继承了Node类,然后执行以下的代码:

MyNode mn = new MyNode(5);
Node n =mn;
n.setData("Hello");
Integer x =mn.data; // 抛出ClassCastException异常

上面的代码在类型擦除之后会转换成以下的形式:

MyNode mn = new MyNode(5);
Node n =(MyNode)mn;
n.setData("Hello");
Integer x =(String)mn.data; // 抛出ClassCastException异常

我们来看看代码是怎么执行的:

(1)n.setData("Hello")调用的事实上是MyNode类的setData(Object)方法(从Node类继承的);

(2)n引用的对象中的data字段被赋值一个String变量。

(3)mn引用的相同对象中的data预期为Integer类型(mn为Node<Integer>类型)。

(4)第四行代码试图将一个String赋值给Integer类型的变量,所以引发了ClassCastException异常。

当编译一个继承了带有參数化泛型的类或借口时,编译器会依据须要创建被称为bridge method的桥接方法。这是类型擦除中的一部分。

上例中MyNode继承了Node<Integer>类。类型擦除之后。代码变为:

class MyNode extends Node {

    //编译器加入的桥接方法
public void setData(Object data){
setData((Integer) data);
} // MyNode的该方法并没有覆写父类的setData(Object data)方法,由于參数类型不一样
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
} // ...
}

注意事项

为了高效地使用泛型,应该注意以下几个方面:

(1)不能用基本类型实例化类型參数

比如

class Pair<K,V> {

    private K key;
private V value; public Pair(K key, V value) {
this.key = key;
this.value = value;
} // ...
}

当创建一个Pair类时。不能用基本类型来替代K,V两个类型參数。

Pair<int,char> p = new Pair<>(8, 'a'); // 编译错误
Pair<Integer,Character> p = new Pair<>(8, 'a'); //正确写法

(2)不可实例化类型參数

比如:

public static <E> void append(List<E> list) {
E elem = new E(); // 编译错误
list.add(elem);
}

可是。我们能够通过反射实例化带有类型參数的对象:

public static <E> void append(List<E> list, Class<E> cls) throws Exception{
E elem = cls.newInstance(); // 正确
list.add(elem);
} List<String> ls = new ArrayList<>();
append(ls,String.class); //传入类型參数的Class对象

(3)不能在静态字段上使用泛型

通过一个反例来说明:

public class MobileDevice <T> {
private static T os; //假如我们定义了一个带泛型的静态字段 // ...
} MobileDevice<Smartphone> phone = new MobileDevice<>();
MobileDevice<Pager> pager = new MobileDevice<>();
MobileDevice<TabletPC> pc = new MobileDevice<>();

由于静态变量是类变量,被全部实例共享,此时,静态变量os的真实类型是什么呢?显然不能同一时候是Smartphone、Pager、TabletPC。

这就是为什么不能在静态字段上使用泛型的原因。

(4)不能对带有參数化类型的类使用castinstanceof方法

public static<E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>){ // 编译错误
// ...
}
}

传给该方法的參数化类型集合为:

S = { ArrayList<Integer>,ArrayList<String> LinkedList<Character>, ... }

执行环境并不会跟踪类型參数,所以分辨不出ArrayList<Integer>与ArrayList<String>,我们能做的至多是使用无限定通配符来验证list是否为ArrayList:

public static void rtti(List<?> list) {
if (list instanceof ArrayList<? >){ // 正确
// ...
}
}

相同,不能将參数转换成一个带參数化类型的对象,除非它的參数化类型为无限定通配符(<?>):

List<Integer> li = new ArrayList<>();
List<Number> ln = (List<Number>) li; // 编译错误

当然,假设编译器知道參数化类型肯定有效,是同意这样的转换的:

List<String> l1 = ...;
ArrayList<String> l2 = (ArrayList<String>)l1; // 同意转变,类型參数没变化

(5)不能创建带有參数化类型的数组

比如:

List<Integer>[] arrayOfLists = new List<Integer>[2]; // 编译错误

以下通过两段代码来解释为什么不行。

先来看一个正常的操作:

Object [] strings= new String[2];
string s[0] ="hi"; // 插入正常
string s[1] =100; //报错,由于100不是String类型

相同的操作。假设使用的是泛型数组。就会出问题:

Object[] stringLists = new List<String>[]; // 该句代码实际上会报错,可是我们先假定它能够执行
string Lists[0] =new ArrayList<String>(); // 插入正常
string Lists[1] =new ArrayList<Integer>(); // 该句代码应该报ArrayStoreException的异常。可是执行环境探測不到

(6)不能创建、捕获泛型异常

泛型类不能直接或间接继承Throwable类

class MathException<T> extends Exception { /* ... */ }    //编译错误

class QueueFullException<T> extends Throwable { /* ... */} // 编译错误

方法不能捕获泛型异常:

public static<T extends Exception, J> void execute(List<J> jobs) {
try {
for (J job : jobs)
// ...
} catch (T e) { // 编译错误
// ...
}
}

可是,我们能够在throw子句中使用类型參数:

class Parser<T extends Exception> {
public void parse(File file) throws T{ // 正确
// ...
}
}

(7)不能重载经过类型擦除后形參转化为相同原始类型的方法

先来看一段代码:

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass()== l2.getClass());

打印结果可能与我们推測的不一样。打印出的是true,而非false。由于一个泛型类的全部实例在执行时具有相同的执行时类(class)。而不管他们的实际类型參数。

事实上。泛型之所以叫泛型,就是由于它对全部其可能的类型參数,有相同的行为;相同的类能够被当作很多不同的类型。

认识到了这一点。再来看以下的样例:

public class Example {
public void print(Set<String> strSet){ } //编译错误
public void print(Set<Integer> intSet) { } //编译错误
}

由于Set<String>与Set<Integer>本质上属于同一个执行时类,在经过类型擦出以后。上面的两个方法会共享一个方法签名,相当于一个方法,所以重载出错。

java泛型具体解释的更多相关文章

  1. Java 泛型具体解释

    在Java SE1.5中.添加了一个新的特性:泛型(日本语中的总称型).何谓泛型呢?通俗的说.就是泛泛的指定对象所操作的类型.而不像常规方式一样使用某种固定的类型去指定. 泛型的本质就是将所操作的数据 ...

  2. JAVA泛型解释

    理解Java泛型最简单的方法是把它看成一种便捷语法,能节省你某些Java类型转换(casting)上的操作: 1 List<Apple> box = ...; 2 Apple apple ...

  3. Java:泛型基础

    泛型 引入泛型 传统编写的限制: 在Java中一般的类和方法,只能使用具体的类型,要么是基本数据类型,要么是自定义类型.如果要编写可以应用于多种类型的代码,这种刻板的限制就会束缚很多! 解决这种限制的 ...

  4. java泛型基础

    泛型是Java SE 1.5的新特性, 泛型的本质是参数化类型, 也就是说所操作的数据类型被指定为一个参数. 这种参数类型可以用在类.接口和方法的创建中, 分别称为泛型类.泛型接口.泛型方法.  Ja ...

  5. 浅谈Java泛型之<? extends T>和<? super T>的区别

    关于Java泛型,这里我不想总结它是什么,这个百度一下一大堆解释,各种java的书籍中也有明确的定义,只要稍微看一下就能很快清楚.从泛型的英文名字Generic type也能看出,Generic普通. ...

  6. Java 泛型,了解这些就够用了。

    此文目录: Java泛型是什么? 通常的泛型的写法示例 类型擦除 为什么要使用Java泛型 通过示例了解PECS原则 一.Java泛型是什么? 官方定义 泛型是Java SE 1.5的新特性,泛型的本 ...

  7. Java泛型总结

    1. 什么是泛型?泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类.可以把类型参数看作是使用参数化类型时指定的类型的 ...

  8. java泛型的讲解

    java泛型 什么是泛型? 泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类.可以把类型参数看作是使用参数化类型时指 ...

  9. Java泛型:类型擦除

    类型擦除 代码片段一 Class c1 = new ArrayList<Integer>().getClass(); Class c2 = new ArrayList<String& ...

随机推荐

  1. 混个脸熟 -- go

    一.第一个项目:hello world src/day1/example1/main.go package main import "fmt" func main(){ fmt.P ...

  2. 【Python】循环语句

    while循环 当条件成立时,循环体的内容可以一直执行,但是避免死循环,需要有一个跳出循环的条件才行. for循环 遍历任何序列(列表和字符串)中的每一个元素 >>> a = [&q ...

  3. [Luogu 2216] [HAOI2007]理想的正方形

    [Luogu 2216] [HAOI2007]理想的正方形 题目描述 有一个a*b的整数组成的矩阵,现请你从中找出一个n*n的正方形区域,使得该区域所有数中的最大值和最小值的差最小. 输入输出格式 输 ...

  4. [转]Oracle 存储过程语法

    转自:http://www.cnblogs.com/chuncn/archive/2009/04/29/1381282.html 存储过程 1  CREATE OR REPLACE PROCEDURE ...

  5. 【HTTP】如何正常关闭连接

    参考:<HTTP权威指南> 所有HTTP客户端.服务器或者代理都可以任意时刻关闭一条TCP传输连接.但是服务器永远无法确定它关闭“空闲”连接的那一刻,在线路那一头的客户端有没有数据要发送. ...

  6. nodejs 中使用 mysql 实现 crud

    首先要使用 mysql 就必须要安装 npm install mysql 然后封装 sql 函数 const mySql = require('mysql'); let connection ; le ...

  7. py2exe打包遇到的问题

    py2exe打包python成.exe文件 打包过程和结果 1.创建setup脚本打包文件,其中设置打包的属性和方法.注意:尽量将被打包文件和此打包脚本放在同目录下(因为在尝试非同目录下时,出现了非可 ...

  8. 复习java第五天(枚举、Annotation(注释) 概述)

    一.枚举 传统的方式: •在某些情况下,一个类的对象是有限而且固定的.例如季节类,只能有 4 个对象 •手动实现枚举类: —private 修饰构造器. —属性使用 private final 修饰. ...

  9. 【技术累积】【点】【git】【10】.gitignore和.gitattributes

    .gitignore 告诉git忽略一些文件,git status会显示不到这些文件的状态. 一般放在项目根目录,以对全局控制,当然可以放在module下: 具体规则主要是: 以行为单位定义忽略文件类 ...

  10. 05-- C++ 类的静态成员详细讲解

     C++ 类的静态成员详细讲解    在C++中,静态成员是属于整个类的而不是某个对象,静态成员变量只存储一份供所有对象共用.所以在所有对象中都可以共享它.使用静态成员变量实现多个对象之间的数据共享不 ...