泛型是在JDK1.5
引入的新的语法,通俗讲,泛型:就是适用于许多许多类型。从代码上讲,就是对类型实现了参数化。
泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译
器去做检查。
// 1. 尖括号 <> 是泛型的标志
// 2. E 是类型变量(Type Variable),变量名一般要大写
// 3. E 在定义时是形参,代表的意思是 MyArrayList 最终传入的类型,但现在还不知道
public class MyArrayList<E> {
private E[] array;
private int size;
}
【规范】类型形参一般使用一个大写字母表示,常用的名称有:
泛型类可以一次有多个类型变量,用逗号分割。
Object
达到的效果(这里不是很准确)。示例:
class MyArrayList<E>{// 代表当前类是一个泛型类,此时的E就是一个占位符而已
private E[] elem;
private int usedSize;
public MyArrayList() {
// this.elem = elem;
this.elem = (E[])new Object[10];//这样的写法不是十分正确
}
public void add(E val){
this.elem[usedSize] = val;
usedSize++;
}
public E get(int pos){
return this.elem[pos];
}
}
public class test02 {
public static void main(String[] args) {
//泛型中尖括号里面的内容不参与类型的组成
//泛型只能接受类,所有的基本数据类型必须使用包装类!
MyArrayList<String> myArrayList = new MyArrayList<>();
System.out.println(myArrayList);
MyArrayList<Integer> myArrayList1 = new MyArrayList<>();
System.out.println(myArrayList1);
MyArrayList<Boolean> myArrayList2 = new MyArrayList<>();
System.out.println(myArrayList2);
}
public static void main2(String[] args) {
MyArrayList<String> myArrayList = new MyArrayList<>();
myArrayList.add("ni");//自动对类型进行检查,不是字符串类型就会报错
myArrayList.add("n");
String ret = myArrayList.get(1);//自动对类型进行强制类型转换
System.out.println(ret);
}
}
裸类型:
裸类型是一个泛型类但没有带着类型实参,例如 MyArrayList
就是一个裸类型:
MyArray list = new MyArray();
裸类型是为了兼容老版本的 API 保留的机制。
Object
是所有类的祖先类,并且父类的引用可以指向子类对象的特定而工作。T
替换为Object
这种机制,我们称为:擦除机制。即 MyArrayList
和 MyArrayList
在运行期间是一个类型。即泛型中尖括号里面的内容不参与类型的组成。Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息。java
中的一种合法语法,标志就是尖括号 <>
,使用
表示当前类是一个泛型类。在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。
语法:
class 泛型类名称<类型形参 extends 类型边界> {
...
}
示例:
public class MyArray<E extends Number> {
...
}
只接受 Number
的子类型作为 E
的类型实参。(E可以是 Number
或者是 Number
的子类)
MyArray<Integer> l1; // 正常,因为 Integer 是 Number 的子类型
MyArray<String> l2; // 编译错误,因为 String 不是 Number 的子类型
如果没有指定类型边界 E,可以视为 E extends Object
。
泛型只有上界,没有下界。
//代表此时传入的E一定要实现compareTo接口,这里也是一种特殊的泛型上界
public class MyArray<E extends Comparable<E>> {
...
}
示例:
public class test01 {
public static void main(String[] args) {
Alg<Integer> alg1 = new Alg<>();
Integer[] arr = {1,34,5,67,9};
System.out.println(alg1.findMax(arr));
Alg<String> alg2 = new Alg<>();
String[] str = {"abd","zge","cde"};
System.out.println(alg2.findMax(str));
}
}
class Alg<T extends Comparable <T>>{
public T findMax(T[] arr){
T max = arr[0];
for (int i = 1; i <arr.length ; i++) {
if(max.compareTo(arr[i]) < 0){
max = arr[i];
}
}
return max;
}
}
语法格式:
方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { ... }
示例:
class Alg2{
public static<T extends Comparable <T>> T findMax(T[] arr){
T max = arr[0];
for (int i = 1; i <arr.length ; i++) {
if(max.compareTo(arr[i]) < 0){
max = arr[i];
}
}
return max;
}
}
public class test01 {
public static void main(String[] args) {
Integer[] arr = {1,34,5,67,9};
//System.out.println(Alg2.findMax(arr));
//可以不类型推导
System.out.println(Alg2.findMax(arr));
}
public class MyArrayList<E> { ... }
// MyArrayList
// MyArrayList 也不是 MyArrayList 的父类型
其在编译时类型都被擦除掉了,其不构成父子类关系。
?
用于在泛型的使用,即为通配符。
通配符是用来解决泛型无法协变的问题的,协变指的就是如果 Student
是 Person
的子类,那么 List
也应该是 List
的子类。但是泛型是不支持这样的父子类关系的。
1、泛型 T
是确定的类型,一旦你传了我就定下来了,而通配符则更为灵活或者说是不确定,更多的是用于扩充参数的范围。
2、或者我们可以这样理解:泛型T就像是个变量,等着你将来传一个具体的类型,而通配符则是一种规定,规定你能传哪些参数。
public static<T> void printList1(ArrayList<T> list) {
for (T x:list) {
System.out.println(x);
}
}
此时上述代码的参数是T,此时的T一定是将来指定的一个泛型参数。
public static void printList2(ArrayList<?> list) {
for (Object x:list) {
System.out.println(x);
}
}
上述代码中使用了通配符,和printList1
相比,此时传入printList2
的具体是什么数据类型,我们是不清楚的。这就是通配符。
语法格式:
<? extends 上界>
<? extends Number>//可以传入的实参类型是Number或者Number的子类
示例:
// 可以传入类型实参是 Number 子类的任意类型的 MyArrayList
public static void printAll(MyArrayList<? extends Number> list) {
...
}
// 以下调用都是正确的
printAll(new MyArrayList<Integer>());
printAll(new MyArrayList<Double>());
printAll(new MyArrayList<Number>());
// 以下调用是编译错误的
printAll(new MyArrayList<String>());
printAll(new MyArrayList<Object>());
示例:
假设有如下关系:
Animal
Cat extends Animal
Dog extends Animal
根据以上的关系,写一个方法,打印一个存储了Animal
或者Animal
子类的list
。
public static void print(List<Animal> list) {
}
这样不可以解决问题,因为print
的参数类型是 List
,就不能接收 List
。
public static <T extends Animal> void print2(List<T> list) {
for (T animal : list) {
System.out.println(animal);
}
}
此时T
类型是Animal
的子类或者自己。该方法可以实现。
通配符实现也可以达到效果:
public static void print3(List<? extends Animal> list) {
for (Animal ani : list) {
System.out.println(ani);
}
}
区别:
1、对于泛型实现的print2
方法,
对T进行了限制,只能是Animal
的子类;比如:传入Cat,那么类型就是Cat。
2、对于通配符实现的print3
方法,首先不用再static
后使用尖括号,其次相当于对Animal
进行了规定,允许你传入Animal
的子类。具体哪个子类,此时并不清楚;比如:传入了Cat,实际上声明的类型是Animal
,使用多态才能调用Cat的toString
方法。
// 需要使用通配符来确定父子类型
MyArrayList<? extends Number> 是 MyArrayList <Integer>或者 MyArrayList<Double>的父类类型
MyArrayList<?> 是 MyArrayList<? extends Number> 的父类型
通配符上界特点:
通配符的上界适合读取数据,不适合写入数据。
ArrayList<Integer> arrayList1 = new ArrayList<>();
ArrayList<Double> arrayList2 = new ArrayList<>();
List<? extends Number> list = arrayList1;
//list.add(1,1);//报错,此时list的引用的子类对象有很多,再添加的时候,任何子类型都可以,为了安全,java不让这样进行添加操作。
Number a = list.get(0);//可以通过
Integer i = list.get(0);//编译错误,只能确定是Number子类
对list不能进行写入:
因为,list
可以引用arrayList1
,或者arrayList2
。所以:
list
中存储的可能是Number
也可能是Number
的子类。此时添加任何类型的数据都不可以,无法确定到底是哪种类型。Number a = list.get(0);
可以通过,此时获取的元素肯定是Number
的 子类。Integer i = list.get(0);
你怎么知道,获取的就是Integer
呢?语法格式:
<? super 下界>
<? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型
示例:
// 可以传入类型实参是 Integer 父类的任意类型的 MyArrayList
public static void printAll(MyArrayList<? super Integer> list) {
...
}
// 以下调用都是正确的
printAll(new MyArrayList<Integer>());
printAll(new MyArrayList<Number>());
printAll(new MyArrayList<Object>());
// 以下调用是编译错误的
printAll(new MyArrayList<String>());
printAll(new MyArrayList<Double>())
MyArrayList<? super Integer> 是 MyArrayList<Integer>的父类类型
MyArrayList<?> 是 MyArrayList<? super Integer>的父类类型
ArrayList<? super Person> list = new ArrayList<Person>();
//ArrayList super Person> list2 = new ArrayList();//编译报错,list2只能引用Person或者Person父类类型的list
list.add(new Person());//添加元素时,只要添加的元素的类型是Person或者Person的子类就可以
list.add(new Student());
Student s = list.get(0);//error
Object s = list.get(0);//可以
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
基本就是类型的首字母大写,除了 Integer
和 Character
。
装箱(装包):把简单类型变成包装类型。
拆箱(拆包):把包装类型变成简单类型。
public static void main(String[] args) {
Integer a = 123;//装箱 装包(隐式的)
//Integer 的范围[-128,127];
int b= a;//拆箱 拆包(隐式的)
System.out.println("a="+a+" "+ "b="+ b);
System.out.println("===============");
Integer a2 = Integer.valueOf(123);//显式的装包
Integer a3 = new Integer(123);//显式的装包
int b2 = a2.intValue();//显式的拆包
double d = a2.doubleValue();//显式的拆包
int i = 10;//显式的初始化
}
在使用过程中,装箱和拆箱带来不少的代码量,所以为了减少开发者的负担,java 提供了自动机制。
自动装箱和自动拆箱是工作在编译期间的一种机制。
int b2 = a2.intValue();//显式的拆包
double d = a2.doubleValue();//显式的拆包
在集合框架中,ArrayList是一个普通的类,实现了List接口,具体框架图如下:
ArrayList
实现了RandomAccess
接口,表明ArrayList
支持随机访问。ArrayList
实现了Cloneable
接口,表明ArrayList
是可以clone
的。ArrayList
实现了Serializable
接口,表明ArrayList
是支持序列化的。Vector
不同,ArrayList
不是线程安全的,在单线程下可以使用,在多线程中可以选择Vector或者CopyOnWriteArrayList
。ArrayList
底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表。public static void main(String[] args) {
// ArrayList创建,推荐写法
// 构造一个空的列表
List<Integer> list1 = new ArrayList<>();
// 构造一个具有10个容量的列表
List<Integer> list2 = new ArrayList<>(10);
list2.add(1);
list2.add(2);
list2.add(3);
// list2.add("hello"); // 编译失败,List已经限定了,list2中只能存储整形元素
// list3构造好之后,与list2中的元素一致
//使用另外一个ArrayList对list3进行初始化
ArrayList<String> list3 = new ArrayList<>(list2);
// 避免省略类型,否则:任意类型的元素都可以存放,使用时将是一场灾难
List list4 = new ArrayList();
list4.add("111");
list4.add(100);
}
ArrayList
可以使用三种方式遍历:for循环+下标
、foreach
、使用迭代器
。
public static void main(String[] args) {
List<String> list = new ArrayList<>();
ArrayList<String> list2 = new ArrayList<>();
list2.add("hello");
list2.add("world");
System.out.println(list2);//打印方式一
System.out.println("============");
for (int i = 0; i < list2.size(); i++) {//打印方式二
System.out.print(list2.get(i)+" ");
}
System.out.println();
System.out.println("==========");
for (String s:list2) {//打印方式三
System.out.print(s+" ");
}
System.out.println();
System.out.println("====迭代器打印方式1======");
Iterator<String> it = list2.iterator();
while (it.hasNext()){
System.out.print(it.next()+" ");
}
System.out.println();
System.out.println("=====迭代器打印方式2=====");
ListIterator<String> it2 = list2.listIterator();//打印方式四,使用迭代器进行打印
while (it2.hasNext()){
System.out.print(it2.next()+" ");
}
}
E remove(int index) //删除 index 位置元素
示例:
public static void main(String[] args) {
ArrayList<String> list2 = new ArrayList<>();
list2.add("hello");
list2.add("bit");
list2.add("haha");
Iterator<String> it = list2.iterator();
while (it.hasNext()) {
String ret = it.next();
if(ret.equals("hello")) {
it.remove();//首先需要使用next方法迭代出集合中的元素 ,然后才能调用remove方法
}else {
System.out.print(ret + " ");
}
}
System.out.println();
System.out.println("========迭代器List相关打印==========");
ListIterator<String> it2 = list2.listIterator();
while (it2.hasNext()) {
String ret = it2.next();
if(ret.equals("hello")) {
it2.remove();//首先需要使用next方法迭代出集合中的元素 ,然后才能调用remove方法
}else {
System.out.print(ret + " ");
}
}
}
boolean add(E e) //尾插 e
示例:
public static void main(String[] args) {
ArrayList<String> list2 = new ArrayList<>();
//CopyOnWriteArrayList list2 = new CopyOnWriteArrayList<>();
list2.add("hello");
list2.add("bit");
list2.add("haha");
//Iterator迭代器没有add方法
/* Iterator it = list2.iterator();
while (it.hasNext()) {
String ret = it.next();
if(ret.equals("hello")) {
it.add();//没有add方法
}else {
System.out.print(ret + " ");
}
}*/
//使用ListIterator迭代器的方法添加元素,会将元素添加到其紧跟着的后面
ListIterator<String> it2 = list2.listIterator();
while (it2.hasNext()) {
String ret = it2.next();
if(ret.equals("bit")) {
it2.add("world");
//若使用list2.add()就会抛出异常,
//但是将上面的list2变成CopyOnWriteArrayList类型就不会报错。
//就可以使用list2.add()的方法就可以
// list2.add("world");
}else {
System.out.print(ret + " ");
}
}
System.out.println("=================");
System.out.println(list2);
}
ArrayList<String> list2 = new ArrayList<>();//不是线程安全的
迭代时使用的是it2.add("world");
CopyOnWriteArrayList<String> list2 = new CopyOnWriteArrayList<>();//是线程安全的
迭代时使用的是list2.add("world");
方法:
void add(int index, E element) //将 e 插入到 index 位置
boolean addAll(Collection<? extends E> c) //尾插 c 中的元素
示例:
public static void main(String[] args) {
ArrayList<String> list2 = new ArrayList<>();
list2.add("a");
list2.add("b");
list2.add("c");
list2.add("d");
list2.add("d");
System.out.println(list2);//add方法默认将字符串放到数组的最后一个位置
list2.add(0,"hello");//在list2的零下标位置加入字符串"hello"
System.out.println(list2);
ArrayList<String> list3 = new ArrayList<>();
list3.add("加入");
list2.addAll(list3);//将list3中的所有元素放到list2中
System.out.println(list2);
}
方法:
E remove(int index) //删除 index 位置元素
boolean remove(Object o) //删除遇到的第一个 o
E get(int index) //获取下标 index 位置元素
E set(int index, E element) //将下标 index 位置元素设置为 element
示例:
public static void main(String[] args) {
ArrayList<String> list2 = new ArrayList<>();
list2.add("a");
list2.add("b");
list2.add("c");
list2.add("d");
list2.add("d");
System.out.println(list2);//add方法默认放到数组的最后一个位置
String ret = list2.remove(0);//删除0下标位置元素
System.out.println(list2);
boolean ret1 = list2.remove("d"); //删除遇到的第一个"d"
//如果要删除的数list2中没有则返回false
System.out.println(ret1); //true
String ret2 = list2.get(2);//获取下标为2位置的元素
System.out.println(ret2);//d
String ret3 = list2.set(2,"hello");//将下标为2位置的元素改为"hello"
System.out.println(ret3);//d
System.out.println(list2);//[b, c, hello]
方法:
void clear() //清空
boolean contains(Object o) //判断 o是否在线性表中
int indexOf(Object o) //返回第一个 o 所在下标
int lastIndexOf(Object o) //返回最后一个 o 的下标
List<E> subList(int fromIndex, int toIndex) //截取部分 list
示例:
public static void main(String[] args) {
ArrayList<String> list2 = new ArrayList<>();
list2.add("a");
list2.add("b");
list2.add("c");
list2.add("d");
list2.add("d");
System.out.println(list2);//add方法默认放到数组的最后一个位置
System.out.println(list2.indexOf("a"));//0 判断a的下标位置
System.out.println(list2.lastIndexOf("d"));//4 判断最后一个d出现的下标位置
List<String> sub = list2.subList(1,3);//[b, c] 截取下标为[1,3)位置的元素,截取的区间范围为左闭右开的
System.out.println(sub);//[b, c]
System.out.println(list2);//[a, b, c, d, d]
System.out.println("===================");
sub.set(0,"p");
System.out.println(sub);//[p, c]
System.out.println(list2);//[a, p, c, d, d]
System.out.println(list2.contains("c"));//true //判断list2当中是否包含字符"c"
list2.clear();//将list2中的元素清空
System.out.println(list2);
如果ArrayList
调用不带参数的构造方法,那么顺序表的大小为0
,当第一次add
的时候,整个顺序表才变为了10
;当这个10
放满了,开始扩容,以1.5
倍的方式进行扩容。
如果调用的是给定容量的构造方法,那么顺序表的大小就是给定的容量,放满了还是以1.5
倍进行扩容。