1、泛型
1.1 为什么要有泛型
泛型就是 标签的意思。看看泛型长什么样
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{}
上面的代码是 ArrayList 类去除类体部分。 <>里面的E就是 泛型的一种写法。泛型一定要放在<> 吗?
看下面的例子
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
上面这个方法是 Collections 类中SynchronizedList内部类的get方法。 这里面的 E 也是泛型,但是没有放在<>里面
泛型长什么样已经知道了,现在问题来了为什么要有泛型呢?它是为了解决什么问题。
ans: 我们发现,在集合类的设计阶段,比如我们定义 ArrayList 这个类时,并不知道 这个ArrayList到底要放什么类型的数据,但是我们知道所有类都是Object类的子类,因此可以使用Object来定义。 但是这样就无法限制只能放置同种类型的数据了。而且使用元素时,还存在类型强转的问题。当混入不是同一种类型的数据,强转会出错。在JDK5.0之前,只能使用Object来定义。
jdk5.0 以后,提供了泛型这个工具来解决这个问题。将元素的类型设计成一个参数,放置<>里面,这个参数就是泛型。泛型也是一个标签,代表某一类型。
在上面第二个例子中, E 没有放在<> 里,因为这个在类的内部使用泛型,而泛型的定义,必须放在<>里面。下面是SynchronizedList的定义
static class SynchronizedList<E>
extends SynchronizedCollection<E>
implements List<E>
1.2 使用
// new ArrayList 对象时,在<>中指定 具体的类型。
List<Integer> list = new ArrayList<>();
list.add(123);
list.add(34);
list.add(7);
Integer i = list.get(2);
System.out.println(i); // 7
一旦泛型被指定,那么 ArrayList 可以添加的元素类型也就确定了。
泛型是可以定义多个的。最常见的是2个。比如 HashMap。先看 HashMap 类的定义去除类体部分。有2个泛型 K 和V 这也说明泛型不是都叫E的。可以自己取名字,一般用大写。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {}
使用:
HashMap<String, Integer> map = new HashMap<>();
map.put("ddd", 2);
System.out.println(map); // {ddd=2}
这就结束了? 泛型还可以嵌套的 看HashMap类中的 entrySet方法,
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
Set<> 的<> 里面是 Map.Entry<K,V> 然后里面还有<>, 里面才是K,V
Map.Entry 实际上是指 Entry 接口,只不过Entry接口是定义在Map接口的内部接口。 因此,Set 里面需要放的是 Entry的实现类对象。
而Entry接口,自己也定义了泛型。 一起写在这里,就是泛型嵌套了。
当我们没有指定泛型时,泛型按照Object类型处理,但是不等同于Object
1.3 自定义泛型结构
1.3.1 泛型类、泛型接口
1、泛型类定义
// 类头定义了 E 这个泛型参数,类体就可以使用了
public class Order<E> {
String name;
int orderId;
// 声明一个 E 类型的变量 e
E e;
public E getE() {
return e;
}
public void setE(E e) {
this.e = e;
}
public Order(String name, int orderId, E e) {
this.name = name;
this.orderId = orderId;
this.e = e;
}
public Order() {
}
@Override
public String toString() {
return "Order{" +
"name='" + name + '\'' +
", orderId=" + orderId +
", e=" + e +
'}';
}
}
上面定义的泛型类只有一个泛型参数E,其实可以定义多个,如果需要的话。
可以看到,泛型类的构造函数并没有<>,和普通类的构造函数的定义是一样的
//使用以下这个泛型类
Order<String> order = new Order<>();
order.setE("sss");
System.out.println(order); // Order{name='null', orderId=0, e=sss}
-
尽管编译时 Order<String> order = new Order<>(); 与Order<Integer> order = new Order<>(); 不同,但是在运行时,只有1个Order类加载到内存中。
-
异常类不能声明为泛型类。下面的写法是错误的,编译就通不过。
// 错误写法
public class MyException<E> extends RuntimeException{}
-
当需要使用泛型创建数组时,怎么写呢?
// 错误写法
// E[] arr = new E[10];
// 正确写法
E[] arr = (E[]) new Object[10];
当某个类继承了 泛型类怎么处理?
-
情况1:继承的时候指定了泛型,子类将不再有泛型
public class SubOrder1 extends Order<Integer>{
}
-
情况2:继承的时候没有指定泛型,任然是一个泛型类
public class SubOrder2<E> extends Order<E>{
}
-
情况3:继承的时候指定了部分泛型,还是一个泛型类。这是多个泛型参数的情况才会出现
public class TwoOrder<E,T> {
int a;
E e;
T t;
public TwoOrder(int a, E e, T t) {
this.a = a;
this.e = e;
this.t = t;
}
public TwoOrder() {
}
}
// 只指定了父类的 一个泛型。
public class SubTwoOrder<T> extends TwoOrder<String, T>{
}
-
情况4 不管有没有指定父类的泛型,都可以定义自己的泛型
public class SubTwoOrder<A,B,T> extends TwoOrder<String, T>{
A a;
}
public class SubTwoOrder<A,B,T> extends TwoOrder{
A a;
}
// 注意,这里的 T 是子类自己的,不是父类的。父类的没有指定等价于
public class SubTwoOrder<A,B,T> extends TwoOrder<Object, Object>{
A a;
}
2、泛型接口定义
接口与类定义泛型没有什么区别。
public interface OrderInterface<E> {
E getE();
}
1.3.2 泛型方法
注意:泛型方法 与方法中使用泛型是不一样的。
下面是方法中使用泛型:
public E getE() {
return e;
}
注意:静态方法中不能使用泛型。因为泛型是造对象时指定的,而静态方法是类加载时创建的。
再看泛型方法的例子:
<T> T[] toArray(T[] a);
泛型方法中的 泛型与类的泛型无关。不要求类是一个泛型类。
当泛型类中有泛型方法时,泛型类定义泛型变量与泛型方法中定义泛型变量不是同一个,即使它们的名称相同。
看下面的代码:
public class Order<E> {
String name;
int orderId;
// 声明一个 E 类型的变量 e
E e;
public <E> E getT(E e){
return t;
}
public Order() {
// 错误写法
// E[] arr = new E[10];
// 正确写法
E[] arr = (E[]) new Object[10];
}
@Override
public String toString() {
return "Order{" +
"name='" + name + '\'' +
", orderId=" + orderId +
", e=" + e +
'}';
}
}
使用
Order<Integer> order = new Order<>();
String ddd = order.getT("ddd");
System.out.println(ddd); // ddd
上面使用的代码中,指定了Order类的泛型是Integer,而调用泛型方法getT时,传入的泛型是String,得到的返回值也是String
虽然Order 类使用的是泛型E变量定义,而且方法getT也是使用的E变量定义,名称一样,但是是2个不同的东西。
1.3.3 泛型在继承的体现
Object obj = new Integer(333);
String str = "333";
// 正确
obj = str;
List<Object> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
// 错误写法!
list1 = list2;
以上说明 泛型中的类型即使是子父类关系,泛型类也不具备子父类关系。list2 不能赋值给 list1,虽然List1中的泛型指定的是Object,是list2指定的String的父类。但是list1 与list2 仍然不具备子父类关系,不能互相赋值。
但是泛型类本身是具有继承关系,指定的泛型相同是可以进行赋值的。
// SubOrder2 是 Order 的子类。都指定了相同的Integer类型
Order<Integer> fa = new Order<>();
SubOrder2<Integer> sub = new SubOrder2<>();
fa = sub;
1.3.4 通配符 ?
通配符就是问号 ?
List<Integer> list1 = null;
List<String> list2 = null;
List<?> list = null;
list = list1;
list = list2;
通配符就是能匹配任意的类型。相等与是 所有的 具体泛型类的公共父类一样。
如:List<A> 和List<B> 是并列关系的,二者的公共父类是 List<?>
用在什么地方呢?
public void print(List<?> list){
for (Object obj : list) {
System.out.println(obj);
}
}
我们写了一个遍历的方法,用上通配符之后,就可以接收指定任意类型的泛型类了。其实,你不写泛型也没事。
public void print(List list){
for (Object obj : list) {
System.out.println(obj);
}
}
以List为例,对于List<?> 就不能向其内部添加数据了,除了null之外。
List<Integer> list1 = new ArrayList<>();
List<?> list = list1;
list.add(null) // 只能添加 null
不让写是为了维护不能随便添加数据这个原则,以前是现在只能添加泛型类型的数据。现在是通配符了,就不让写了。
但是是可以读的。
List<Integer> list1 = new ArrayList<>();
list1.add(555);
list1.add(123);
List<?> list = list1;
Object o = list.get(1);
System.out.println(o); // 123
-
有限制条件的通配符
上限 extends :使用时指定的类型必须是继承某个类或者实现某个接口。
下线super:使用时指定的类型不能小于操作的类
<? extends Number> 泛型类型只允许Number以及子类
<? super Number> 泛型类型必须为 Number或者Number的父类
看例子:
// 有一个 Order 类
public class Order<E> {
String name;
int orderId;
}
// Order的子类 SubOrder2
public class SubOrder2<E> extends Order<E>{
}
// list1 能作为 泛型是Order或者Order的子类 的父类
List<? extends Order> list1 = new ArrayList<>();
// list2 能作为 泛型是Order或者Order的父类 的父类
List<? super Order> list2 = new ArrayList<>();
List<Order> list3 = new ArrayList<>(); // 泛型是 Order 的 List
List<SubOrder2> list4 = new ArrayList<>();// 泛型是 Order 的 子类 SubOrder2 的 List
List<Object> list5 = new ArrayList<>();// 泛型是 Order 的 父类 Objec 的 List
list1 = list3; // 正确
list1 = list4; // 正确
list1 = list5; // 错误 list1 不能接收泛型是 Order的父类的 List 对象
list2 = list3; // 正确
list2 = list5; // 正确
list2 = list4; // 错误 list2 不能接收泛型是 Order的子类的 List 对象
有条件的通配符的对象读写分析:
首先是读,都可以读。不管有没有条件。
写的话需要分情况,null都是可以写的。
list2.add(new Order()); // 正确
只有使用 super 限定时,只能放Order及其父类,那么添加进去的最小必须是 Order类。
只有是添加Order类本身时,才允许被添加。
如果是允许添加的是Order其他的父类,无法保证都能被Order接收。只有是添加Order类,一定能被Order接收。
2、File 类
File 类即可以表示一个文件,也可以表示一个文件夹。
File 类 在 java.io 包下
File类在JDK8.0 有 2241 行代码。简要看看
1、构造器
可以看到,File类共有 6个有参构造器,没有无参构造器,其中前2个是私有构造器。我们在外面能使用 的只有后4个构造器。
上图中第二列的 红色的锁代表是 private权限,绿色的锁且打开的是 public 权限。
最常用的是 第三个,参数是String 类型的 路径。可以是相对路径,也可以是绝对路径
// 相对路径 取的是当前路径。在idea中,是当前module的根路径
File file = new File("hello.txt");
// 绝对路径
File file1 = new File("G:\\study\\leetCode\\JavaBaseStudy");
new了对象之后,只是内存中的 普通对象。并没有与硬盘中的文件进行对应
上述绝对路径的写法中,分隔符使用了 "\\" 主要是 "\" 在java中 表示转义的意思。把本来是转义字符再转义一下,变成普通字符"\"
在 linux 中 分隔符用 / 而在windows中 用的是 \
为了避免写错,File类提供了一个常量 separator 来动态根据操作系统获取分隔符。
File.separator
第4个构造器,第一个参数写 路径,第二个参数写 第一个参数路径下的文件夹或者文件。
File file2 = new File("G:\\study\\leetCode\\JavaBaseStudy\\", "hello.txt");
第5个构造器, 第一个参数传File 类型的对象,第二个参数是这个对象的路径的子路径或文件
File file3 = new File("G:\\study\\leetCode\\JavaBaseStudy\\");
File file4 = new File(file3, "hello.txt");
System.out.println(file4);
//输出: G:\study\leetCode\JavaBaseStudy\hello.txt
最后一个构造器: 接收的是一个URI 对象
public File(URI uri)
上面是 这个构造器的方法头。接收的是URI 类。后续再讲
2.1 File类的常用方法
-
File getAbsoluteFile() 获取文件的完整路径
File file = new File("hello.txt");
File file1 = new File("G:\\study\\leetCode\\JavaBaseStudy\\hello.txt");
System.out.println(file); // hello.txt
// 获取完整路径,返回值还是 File类型的对象
File absoluteFile = file.getAbsoluteFile();
System.out.println(absoluteFile); // G:\study\leetCode\JavaBaseStudy\hello.txt
-
getPath、getParent、length、lastModified方法
getPath 获取路径
getParent 获取父路径
length 长度
lastModified 最近一次修改时间戳
File file = new File("hello.txt");
String path = file.getPath();
String parent = file.getParent();
long length = file.length();
long l = file.lastModified();
System.out.println(path); // hello.txt
System.out.println(parent);// null
System.out.println(length);// 7
System.out.println(l);// 1630026081316
File file = new File("G:\\study\\leetCode\\JavaBaseStudy\\hello.txt");
String path = file.getPath();
String parent = file.getParent();
long length = file.length();
long l = file.lastModified();
System.out.println(path); // G:\study\leetCode\JavaBaseStudy\hello.txt
System.out.println(parent);//G:\study\leetCode\JavaBaseStudy
System.out.println(length);//7
System.out.println(l);// 1630026081316
可以看到,当是相对路径时,是没有parent的。而绝对路径可以获取上一级目录
此时还是内存级别的操作,不涉及到硬盘中是否有这个文件。即 new File对象时,这个路径可以是不存在的。
对于相对路径写法,想要获取上一级目录,可以先获取完整路径getAbsoluteFile,再调用getParent方法。
-
String[] list() 获取指定目录下的所有文件或者文件目录的名称数组。
File[] listFiles() 获取指定目录下的所有文件或者文件目录的文件数组。
File file = new File("G:\\study\\leetCode\\JavaBaseStudy\\hello.txt");
String[] list = file.list();
File[] files = file.listFiles();
System.out.println(list); // null
System.out.println(files); // null
// 之所以是null 是因为file是一个文件,而不是一个文件夹
File file = new File("G:\\study\\leetCode\\JavaBaseStudy\\");
String[] list = file.list();
File[] files = file.listFiles();
for (String s : list) {
System.out.println(s);
}
for (File file1 : files) {
System.out.println(file1);
}
// 输出
hello.txt
JavaBaseStudy.iml
pom.xml
src
target
G:\study\leetCode\JavaBaseStudy\hello.txt
G:\study\leetCode\JavaBaseStudy\JavaBaseStudy.iml
G:\study\leetCode\JavaBaseStudy\pom.xml
G:\study\leetCode\JavaBaseStudy\src
G:\study\leetCode\JavaBaseStudy\target
因此,这2个方法对文件目录才有意义。而且要求这个目录需要存在,不存在会报错
-
boolean renameTo(File dest) 文件重命名方法
这个方法接收一个 File 对象。
要想保证成功,需要保证 原文件是存在的,且目标文件对象是不存在的。
File file = new File("hello.txt");
// 硬盘中并没有这个he.txt文件
File he = new File("he.txt");
boolean b = file.renameTo(he);
System.out.println(b); // true
-
isDirectory 判断 File对象是否是 目录
isFile 判断 File对象是否是 文件
exists 判断 File对象是否在硬盘存在
canWrite 判断文件是否可写、canRead 可读、canExecute 可执行
isHidden File对象是否是隐藏的
boolean directory = file.isDirectory();
boolean file1 = file.isFile();
boolean exists = file.exists();
boolean b = file.canRead();
boolean b1 = file.canWrite();
boolean b2 = file.canExecute();
boolean hidden = file.isHidden();
使用时,应该先调用 exists 方法判断是否存在,再判断是否是 文件或者文件夹。
-
boolean createNewFile() 创建新文件,如果已经存在则不会创建。返回是否创建成功。
// 该文件已经存在
File file = new File("hello.txt");
boolean newFile = file.createNewFile();
System.out.println(newFile); // false
-
mkdir() 创建目录,如果不存在
mkdirs 创建目录,如果这个目录的上层目录也不存在,会一并创建。
File file = new File("hello.txt");
boolean mkdir = file.mkdir();
boolean mkdirs = file.mkdirs();
// file对象已经存在了,就不会再创建
System.out.println(mkdir); // false
System.out.println(mkdirs); // false
-
delete() 移除文件 返回是否删除成功
当删除文件时,需要文件存在才能删除成功
当删除文件夹时,需要是空文件夹才能删除成功。 如果需要删除非空目录,需要遍历该目录,将里面的东西全部删除之后才能删除这个目录。
// he.txt 是不存在的 hello.txt是存在的
File file = new File("he.txt");
File file1 = new File("hello.txt");
boolean delete = file.delete();
boolean delete1 = file1.delete();
System.out.println(delete); // false
System.out.println(delete1); // true
File类 不能对文件进行读写操作,进行读写操作必须使用流来进行。
File对象通常是传递给流的构造器,告诉流对那个文件进行操作。