泛型的实质就是将数据的类型参数化,可以在定义类,接口,方法时使用泛型。优势就是将检查数据类型转换时的潜在错误由运行期提前到编译期。减少了很多错误。
当这种类型参数在类、接口以及方法中声明时,就称为泛型类,泛型接口和泛型方法。
格式如下:
//泛型类的定义:
[修饰符] class className <T>
//泛型接口的定义:
[public] interface interName <T>
//泛型方法的定义:
T methodName();
void methodName(T e)
定义泛型之后,就可以在代码中使用类型参数T来表示某一种类型而非数据的值。泛型的实际参数必须是类类型,利用泛型类创建的对象称为泛型对象,这个过程也称为泛型的实例化。而泛型的概念实际上是基于“类型也可以像变量一样参数化”,所以泛型也称为参数多态。
在泛型实例化,也就是使用泛型定义的类创建对象时,可以根据不同的需求给出给出类型参数的具体类型,而在调用方法传递或返回参数时,可以不用进行类型转换,直接用类型参数来代替参数类型或返回值的类型。
说明:在实例化泛型类的过程中,实际类型必须是引用类型,不能用如int,double或char等基本类型来代替
例:
public class Test<T>{
private T obj;//定义泛型类的成员变量
public T getObj(){ //定义方法getObj
return obj;
}
public void setObj(T obj){ //定义方法setObj
this.obj=obj;
}
public static void main(String[] args){
Test<String> name=new Test<String>();
Test<Integer> age=new Test<Integer>();
name.setObj("GR");
String newName=name.getObj();
System.out.println("姓名:"+newName);
age.setObj(20); //自动拆装包
int newAge=age.getObj();
System.out.println("年龄:"+newAge);
}
}
通过例子可以看出,泛型的定义并不复杂,可以将T看做一种特殊的“变量”,该变量的“值”在创建泛型对象时就指定,它可以是除了基本类型之外的任意类型,包括类,接口甚至是一个类型变量。
当一个泛型有多个类型参数时,每个类型参数在该泛型中都应该是唯一的。
例如:不能定义形如Map
可以使用类型参数声明泛型方法。一个方法是否是泛型方法与其所在的类是否是泛型类没有关系。要定义泛型方法,只需将泛型的类型参数置于返回值类型前面即可。在Java中任何方法包括静态方法和构造方法都可声明为泛型方法。泛型方法除了定义不同,调用时与普通方法样。
说明:在调用泛型方法时,为了能强调是泛型方法,也可将实际类型放在尖括号内作为方法名的前缀,如例5,6行前可加上
一般来说,编写 Java泛型方法时,返回值类型和至少一个参数类型应该是泛型,而且类型应该是一致的,如果只有返回值类型或参数类型之一使用了泛型,这个泛型方法的使用就大大地受到限制,基本限制到与不用泛型-一样的程度。所以推荐使用返回值的类型和参数的类型一致的泛型方法。Java泛型方法广泛使用在方法返回值和参数均是容器类对象时。
注意:若泛型方法的多个形式参数使用了相同的类型参数,并且对应的多个实参具有不同的类型,则编译器会将该类型参数指定为这多个实参所具有的“最近”共同父类直至Object.
例:
public class Test{ //定义一般类,即非泛型类
public static void main(String[] args){
Integer[] num={1,2,3,4,5};
String[] str={"红","橙","黄","绿","青","蓝","紫"};
Test.display(num); //用类名调用静态泛型方法
Test.display(str);
}
public static<E> void display(E[] list){ //定义泛型方法
for(int i=0;i<list.length;i++){
System.out.print(list[i]+" ");
}
}
}
运行结果
当使用泛型类时,必须在创建泛型对象的时候指定类型参数的实际值,而调用泛型方法时,通常不必指明参数的类型,因为编译器有个功能叫做:类型参数推断,此时编译器会为我们找出具体的类型。类型推断只对赋值操作有效,其他时候并不起作用。
在泛型机制中引入了通配符“?”的概念,其主要作用有两个方面,一是用于创建可重新赋值但不可修改其内容的泛型对象,二是用在方法的参数中,限制传人不想要的类型实参。
当需要在一个程序中使用同一个对象名去引用不同的对象时,这时就需要使用通配符“?”创建泛型类对象.但条件是被创建的这些不同泛型对象的类型实参必须是某个类或是继承该类的子类或是实现某个接口的类。也就是说,只知道通配符“?”表示是某个类或是继承该类的子类或是实现某个接口的类,但具体是什么类型不知道。如下面语句是用泛型类创建泛型类对象:
泛型类名 extends T> o = null; //声明泛型类对象。
其中“? extends T”表示是T或T的未知子类型或是实现接口T的类。所以在创建泛型对象o时,若给出的类型实参不是类T或T的子类或是实现接口T的类,则编译时报告出错。通配符“?”除了在创建泛型类对象时限制泛型类的类型之外,还可以将由通配符限制的泛型类对象用在方法的多数中以防止传人不允许接收的类型实参。
直接用通配符>创建泛型对象,有两个特点:
(1)具有通用性,即该泛型类的其他对象可以赋值给用通配符“?”创建的泛型对象,因为“?”等价于”? extends Obiect",反之不可。
(2)用通配符“?”创建的泛型对象,只能获取或删除其中的信息,但不可为其添加新的信息。
被定义为泛型的类或接口可被继承和实现。例如:
public class ExtendClass < T1 >{
}
class SubClass
}
如果在SubClass类继承ExtendClass类时保留父类的类型参数,需要在继承时指明,如果没有指明,直接使用extendsExtendClass语句进行继承声明,则SubClass类中的T1、T2和T3都会自动变为Object,所以在一般情况下都将父类的类型参数保留。
在定义泛型接口时,泛型接口也可被实现。如下面的语句:
interface in < T1 >{
}
class SubClass< T1, T2> implements in< T1 >{
}
容器类是java以类库的形式来供用户开发时可以直接使用的各种数据结构。不仅可以存储数据,还支持访问和处理数据的操作。例如数组就是一种简单的数据结构,除了数组外Java还以类库的形式提供了许多其他的数据结构,这些都被称为容器类或者集合类。
Java容器框架中有两个名称分别为Colleetion 和Set的接口,可将Collection译为容器,而将Set译为集合。Java容器框架提供了一些现成的数据结构可供使用,这些数据结构是可以存储对象的集合,在这里对象也称为元素。从JDK1.5开始,容器框架全部采用泛型实现,且都存放在java. util包中。容器框架中的接口及实现这些接口的类的继承关系如下图所示。Java容器框架结构由两棵接口树构成,第一棵树根节点为Collection接口,它定义了所有容器的基本操作,如添加、删除、遍历等。它的子接口Set、List等则提供了更加特殊的功能,其中Set的对象用于存储-组不重复的元素集合,而List的对象用于存储一个由元素构成的线性表。第二棵树根节点为Map接口,它保持了“键”到“值”的映射,可以通过键来实现对值的快速访问。
Collection接口通常不能直接使用,但该接口提供了添加元素、删除元素、管理数据的方法。由于Set接口和List接口都继承了Collection 接口,因此这些方法对集合Set与列表List是通用的。方法默认为public abstract。由于容器框架全部采用泛型实现,所以我们以泛型的形式给出相应的方法,即带类型参数。
Collection接口常用方法
方法 | ASCII |
---|---|
int size | 返回容器中元素的个数 |
boolean add() | 向容器中添加元素 |
int hashCode() | 返回哈希值 |
void clear | 删除容器中所有的值 |
还有很多方法就不列出了,可查看api手册
List接口是Collection的子接口,它是一种包含顺序的线性表。
List 是一个元素有序、且可重复的集合,集合中的每个元素都有其对应的顺序索引,从0开始
List 允许使用重复元素,可以通过索引来访问指定位置的集合元素。
List 默认按元素的添加顺序设置元素的索引。
List 集合里添加了一些根据索引来操作集合元素的方法
实现List接口的类主要有两个,实现类ArrayList和LinkedList,ArrayList是实现了基于动态数组的数据结构,对象存储在连续的位置上。而LinkedList基于双链表的数据结构,链表中的每个节点都包含了前一个和后一个元素的引用。
对于随机访问get和set,ArrayList绝对优于LinkedList,因为LinkedList要移动指针。
对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。
例:利用LinkedList类构造一个先进后出的堆栈
import java.util.LinkedList;
import java.util.Scanner;
class StringStack{
private LinkedList <String> ld =new LinkedList<String>();
public void push(String name){ //入栈
ld.addFirst(name);
}
public String pop(){ //出栈
return ld.removeFirst(); //获取并移出堆栈中的第一个元素
}
public boolean isEmpty(){ //判断堆栈是否为空
return ld.isEmpty();
}
}
public class Test{
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
StringStack stack = new StringStack();
System.out.println("请输入数据(quit结束)");
while(true){
String input = sc. next(); //入栈
if( input. equals("quit"))
break;
stack. push( input);
}
System.out.println("先进后出的顺序:");
while( !stack. isEmpty())
System. out. print(stack. pop()+" "); //出栈
}
}
第一种,可以用foreach循环语句,绝大多数容器都支持这种方式
第二种,利用Collection接口中定义的toArray()方法将对象转化为数组,再利用循环来访问
第三种,利用size()和get()方法进行遍历,先获取容器内元素的总个数,然后依次取出每个位置上的元素并访问
第四种,利用迭代器
接口:Iterator
Collection接口里提供了获取Iterator的实现类方法
abstract Iterator iterator();
Collection接口的实现类以内部类的方式实现了Iterator的
实现类,重写iterator()
迭代器接口提供的方法有:
boolean hasNext():询问集合内是不是有下一个元素
E next():取出下一个元素(此时,光标会移动)
void remove():移除集合中的元素
注意:使用迭代器期间,如果做删除操作,不能使用集合提供的删除方法,否则会影响迭代次数。 应该使用迭代器自己的删除方法
Set集合中的元素是无序的(取出的顺序与存入的顺序无关)
Set集合中的元素不能重复(即不能把同样的东西两次放入同一个Set容器中)
Set 判断两个对象是否相同,不是使用 == 运算符,而是
调用 equals 方法进行比较。因此,添加到Set集合中的
元素类型必须重写equals方法
HashSet 是 Set 接口的典型实现,
大多数时候使用 Set 集合时都使用这个实现类。
HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取和查找性能。
HashSet 具有以下特点:
LinkedHashSet 是 HashSet 的子类
LinkedHashSet 集合根据元素的 hashCode 值来决定元素的存储位置,但它同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
LinkedHashSet 性能插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。
LinkedHashSet 不允许集合元素重复。
TreeSet 是 SortedSet 接口的实现类,
TreeSet集合是用来对元素进行排序的,同样他也可以保证元素的唯一。
TreeSet 可以确保集合元素处于排序状态。
TreeSet 支持两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序。
Map是集合框架中的另一个父接口,用于保存具有映射关系的数据,这样的数据称之为key-value键值对。key可以看成是value的索引。
特点如下:
key和value也必须是引用类型的数据
作为key的对象在Map集合中不允许重复
key可以为null
key和value之间存在单向一对一关系,通过指定的key总能找到唯一,确定的value
根据内部数据结构的不同,Map接口有多种实现类,其中常用的有内部为hash表实现的HashMap和内部为排序二叉树实现的TreeMap
HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置。HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突
当向HashMap中添加元素时,我们先通过key的hash值经过一系列的计算算出所在数组中的位置,如果此位置上已经有元素了,那么我们在通过equals方法与此位置上的单向链表内的元素一一比较,如果有相同的key,就会替换原有的key-value,如果没有,添加在此单向链表中. 如果此位置上没有元素,就直接存储在此位置上对应单向链表的头节点上。
现在的问题就是怎么解决作为key对象的hash值有可能与其他key的hash值冲突问题(此时应该尽量避免单向链表的产生)
怎么解决:
Object类的hashCode方法是:返回对象的散列值,
这个值是根据对象的内存地址经过hash算法处理后
的一个int值。(如果不重写,这个算法不可控,相撞的几率就非常大)
因此,我们需要重写,让相撞的几率更小。
package com.hyxy.map;
import java.util.HashMap;
import java.util.Map;
public class MapDemo01 {
public static void main(String[] args) {
/*存储多个人的姓名和相应年龄*/
Map<String,Integer> maps =
new HashMap<String,Integer>();
/* V put(K k,V v)
* 用于存储键值对数据
* 如果再次存储key相同的key-value数据
* 会覆盖之前的value值
* */
maps.put("张三", 23);
maps.put("李四", 24);
System.out.println(maps);
/*boolean isEmpty()
* 查看map中是否为null
* */
System.out.println(maps.isEmpty());//false
/*void clear()
* 清空map中的键值对
* */
//maps.clear();
//System.out.println(maps.isEmpty());
/*V get(K k)
*通过key获取value,如果没有key对象,返回值为
*null
* */
Integer age = maps.get("张三");
System.out.println("张三的age:"+age);
age = maps.get("王五");
System.out.println("王五的年龄:"+age);
//再次存储 张三-->26
maps.put("张三", 26);
System.out.println("maps的大小:"+maps.size());
System.out.println("张三的年龄:"+maps.get("张三"));
/*boolean containsKey(K k)
*判断是否包含指定的key值
* */
boolean f = maps.containsKey("张三");
System.out.println("f:"+f);
if(maps.containsKey("赵六")) {
age = maps.get("赵六");
System.out.println("赵六的age:"+age);
}else {
System.out.println("没有赵六这个人");
}
/*containsValue(V v)
* 判断是否包含指定的value值
* */
System.out.println("value值是否有24:"+maps.containsValue(24));
/*V remove(K k)
* 通过指定的key移除key-value键值对,
* 返回value值。
*/
Integer value = maps.remove("张三");
System.out.println("maps的大小:"+maps.size());
System.out.println("被移除的value:"+value);
}
}