IO流
我们的电脑上有硬盘和内存,硬盘里有文件,内存里也有文件,将硬盘上的文件传入到内存中的过程我们称之为读(Read),又叫输入(Input),输入的过程中必然会有数据的流动,在这个流动我们称之为输入流(InputStream),反之如果要把内存中的文件传入到硬盘上,我们就叫写(Write),又叫输出(Output),所产生的流动的过程我们就称之为输出流(OutputStream)
最后我们我们说的IO流,其实是(Input Output Stream)的缩写,意为在内存和硬盘间互相读写文件的过程
最后值得一提的是通过IO可以完成对硬盘的读和写,然后IO流是只相对于内存而言的,往内存里写入文件就叫写,往内存里输出文件就叫读
IO流的分类
IO流的分类有两种方式,第一种是按照输入输出的方式来分,往内存里输入叫输入流,往内存外输出叫输出流,这是按照输入输出的方式来分类的
第二种是按照读取数据的方式来分,有的流是按照字节的方式读取字符的,一次读取一个字节byte,等同于一次读取8个二进制位,这样的读取方式是什么文件都可以读取的,是万能的,我们将其称之为字节流
而有的流是按照字符的方式来读取数据的,一次只能读取一个字符,这种流的存在是为了方便读取普通文本文档的读取而存在的,也就是只能读取txt文档。这种流我们称之为字符流
流的四大家族
java.io流这块有四大家族
java.io.InputStream 字节输入流
java.io.Outputstream 字节输出流
java.io.Reader 字符输入流
java.io.Writer 字符输出流
直接看其类名最后的词,只要是以stream结尾的,都是字节流,只要是以reader或者是writer结尾的,都是字符流,通过英语自己分输入和输出
这四大家族的首领都是抽象类(abstract class)
close与flush
java中所有的流都是实现了java.io.Closeable接口的,也就是说,所有的流都是有close();方法的,都是可关闭的,流说到底还是一个管道,我们使用完之后一定别忘了要将流关闭,否则会占用很多资源
所有的输入流都是实现了java.io.Flushable接口的,都是可刷新的,都有flush();方法,这个方法表示的意思是刷新管道,刷新表示的意思是将流中未输出的数据强制输出出去,我们在用完输入流之后只要记得使用flush();方法将剩余未输出数据输出出去,否则可能会导致数据丢失
FileInputStream
java.io.FileInputStream可以找到这个构造方法FileInputStream(String name),这个方法的作用是传入一个文件路径,这样来创建一个输入流
表示路径既可以用/,也可以用\来进行目录分割
FileInputStream类中有read()方法,可以从输入流中读取一个数据字节
当我们没有调用read()方法前,read的指针默认停留在其第一个字节的前一位,当我们调用之后,指针会指向a并取出a,返回a所代表的int类型的值,也就是97,继续调用就重复此过程,当调用到末位的时候,由于啥都没有了,所以会返回-1
通过构建while循环可以实现循环读,从而实现读入文件的数据
read()还有重载方法,可以传入byte[]数组,来实现一次读入多份数据,同时调用String类里的byte数组转为String字符串里的构造方法,也就是String(byte[] bytes),我们就可以将该byte数组转换为字符串输出,让每次转换的字符串长度是从0开始到读取到的长度为止,利用String类里的String(byte[] bytes,int offset, int length)重载方法完成目标
最终版本的代码如下,该版本需要记忆,以后均是使用该版本
csharp
public class test {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("此处存放文件地址");
//先创建空间为4的byte数组
byte[] bytes = new byte[4];
int readCount = 0;
while ((readCount = fis.read(bytes))!=-1){
System.out.print(new String(bytes,0,readCount));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {//用finally语句进行流的关闭
if(fis!=null){//若流不等于null则关闭流,若等于则无需关闭
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
FileInputStream的其他方法
int available()方法会返回文件中还未读取的元素数量,那么这个方法有什么用呢?一个简单的用法是可以预先读取文件中的元素的数量,然后直接创建适合它的byte数组的空间,然后我们只要一次读取就可以读取出全部文件内容了,但是要注意这个方法并不适用内容较多的文件,因为byte数组指定的空间不能太大
long skip(long n)方法可以跳过文件中指定的n个字节的数据不读取,从而去读取跳过数据之后的数据
FileOutputStream
FileOutputStream是字节输出流,其作用则是将内存里的内容写入到硬盘中
其下的write(byte[] b)方法可以将指定的byte数组的内容全部写入到文件中
我们先创建了一个输出流,并给我们的文件指定了名字,但是没有具体指定位置,那么我们的文件就会默认生成在根目录中,接着我们自己创建了一个bytes数组,然后利用write();方法将bytes传入,再调用了指定位置的方法又写入了ab,那样我们的文件最终就会显示abcdab。当然,不要忘了写完之后要再调用fos.flush();方法刷新我们的输出流
但是问题是,new FileOutputStream()方法是会先将文件清空再写入的,这个方法也是比较危险的,因为他会先清空我们的文件内容,因此最好不调用,免得调用一次咱们重要的文件内容全无了那就直接裂开了
那如果我们是希望我们能够在文件中不断地写入我们的内容我们应该要怎么办呢?我们可以看看在FileOutputStream类里的FileOutputStream(String name,boolean append)构造方法
该构造方法就是能够实现我的目标的构造方法,调用它也很简单,直接在原先的构造方法上再加一个true就可以了
假设我们要传入一个写入一个中文字符串,那我们可以该字符串转换成byte数组,然后通过write方法传入就可以了
文件复制
假设我们要从D盘复制一份视频到C盘,那么我们实际的复制过程其实是先读取D盘中文件的数据到内存,然后内存再将这些数据写入到C盘
如果要在代码中实现复制,自然也应该要同时实现输入和输出,那么我们可以构造代码如下
java
public class test {
public static void main(String[] args) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
//创建一个输入流对象,用于读取
fis = new FileInputStream("需要复制的文件路径");
//创建一个输出流对象,用于写入
fos = new FileOutputStream("需要复制到的位置");
//最核心的:一边读,一边写
byte[] bytes = new byte[1024 * 1024];//1MB(一次最多拷贝1MB)
int readCount = 0;//定义用于保存数量的变量
while ((readCount = fis.read(bytes))!=-1){
//判断符号的代码不但是判断条件,同时还执行了读的这一过程
fos.write(bytes,0,readCount);
//上一行代码主要是执行写的过程
}
//刷新,输出流最后一定要刷新
fos.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭输入流
if(fos!=null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//关闭输出流
if(fis!=null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//之所以要分开写,是因为如果合起来写,如果第一个报了异常
//那么第二个就不会执行了,那么第二个就不用关闭了,所以为
//了安全起见所以我们要分开写
}
}
}
按照这个格式我们只要填入我们需要复制的文件和要复制到的位置然后运行该代码就可以了
FileReader
字节的输入输出可以用于所有类型,虽然实用性广,但是效率方面上可能会有些不太高效,字符的输入输出虽然只可以用于txt的文本格式,但是其效率却会比较可观
FileReader的方法其实跟我们之前学习的FileInputStream类的方法是差不多的,唯一与之不同的是,在后者里我们是使用byte数组,而在前者,我们是要使用char数组
FileWriter
FileWriter的类中查看其方法,发现啥方法都没有,那我们就去其父类的父类,Reader类中查看,能够发现其存在写入字符串的方法
分别用于写入单个字符,写入字符串,以及写入字符串的一部分,有了这三个方法,我们在文本里写入字符的时候就方便多了
字符流只能读写文本,不能读写其他类型的内容
带有缓冲区的字符流
BufferedWriter
缓冲流有四个,但是我们这里重点将带有缓冲区的字符流,也就是java.io.BufferedReader和java.io.BufferedWriter
缓冲流的意思就是不用自动以数组用于读写的意思,比非缓冲流用起来要方便得多
BufferedReader
进入BufferedReader类里,能看到其构造方法需要传入一个Reader对象,Reader是抽象类,因此传入其子类实现FileReader
如果一个流的构造方法需要传入一个流的时候,被传入的流叫节点流,而接受流的流叫做包装流/处理流
当我们需要关闭流的时候,只需要调用关闭包装流的close();方法就可以了,因为当我们调用外部包装流的close();方法之后,经过源码的层层递进,最终会在底层调用节点流的close();方法
BufferReader的方法,其中最重要的方法是这个String readLine();方法,该方法的作用是读取一个文本行并返回一个String类的对象,有了这个方法我们就可以一行一行地读取了,不用跟以前一样用数组一组一组地进行读取了
该方法如果读取到行的最后没有行可以读取了,那么其会返回null,知道了这个之后我们就可以构建关于这个方法的循环了
转换流
BufferedReader的构造方法是要求传一个字符流进去的,而如果我们创建了一个字节流,那我们将其传入的话编译器肯定是不会通过的,那此时我们就可以使用转换流来将字节流转换成字符流之后再传入
对于第一个转换流的方法而言,in是结点流,而reader是包装流,但是对于第二个缓冲流的方法而言,reader是节点流,而br是包装流
这些代码合并起来写到一起,括号内只需要填入我们所需要的读取的文件路径即可
数据流
利用数据流可以实现简单的加密操作,不过很少用,只作为了解
DataOutputStream
同样当我们调用其构造方法时,需要传入一个流,需要传入一个OutputStream流进入,而正好该流是节点流,因此我们传入其子类FileOutputStream,选定一个文件路径即可
我们可以调用其下特定的写入数据的方法来写入对应的数据,写入的数据无法用txt文件打开进行查看
DataInputStream
我们要从该文件读入数据的话,那么我们只能够使用java.io.DataInputStream来进行读入,而且我们必须按照我们之前写入的顺序来进行读入才能够顺利读入,否则是无法正常取出数据的
标准输出流
java.io.PrintStream是标准的字节输出流,默认输出到控制台,我们经常写的打印代码其实是直接调用了流中的println方法进行写入操作,然后其会默认输出在控制台上,因为System.out返回的是一个PrintStream的标准输出流
可以利用标准输出流来改写日志,先用代码指定了一个生成路径,然后利用System.SetOut();将输出方向修改至我们指定的log文件,这样我们下面输出的字符就会生成到我们指定的路径的文件上,再加入一些逻辑处理代码即可实现简单的日志记录
File类的理解和常用方法
File类的直接父类是Object,其和四大家族没有什么关系,File也不是流,File就是文件和目录路径名的抽象描述,任何一个路径都可以是file
File(String pathname)方法可以传入一个路径来创造一个File对象
exists();方法可以用于判断指定目录中是否有该文件,若有则返回true,反之则返回false
createNewFile();方法可以在指定目录下创建指定的文件,可以通过前面的exists();方法联立使用,让我们指定的位置处如果没有指定文件那么就生成一个指定文件
mkdir();方法则是以目录的形式将文件生成出来,同样可以搭配exists();方法使用
使用mkdirs();方法还可以创建多重目录
getParent();方法可以获取文件的父路径,并且以字符串的形式返回
getParentFile();方法同样可以获得其父路径,但是此时是以File的形式返回的
getAbsolutePath();方法,可以获得文件的绝对路径
getName();方法,可以获取文件名并以字符串的形式返回
isDirectory();方法和isFile方法可以分别用于该文件是一个目录还是一个文件,前者判断是否是一个目录,后者判断是否是一个文件
lastModified();方法,调用该方法可以获取文件最后一次修改的时间,这个时间是以1970到最后一次修改的时间的毫秒数返回的,最好将其转换为便于程序员阅读的格式
先调用lastModified();方法获得总毫秒并将总毫秒传入日期对象的构造方法中创造一个日期对象,接着调用日期格式方法创造一个日期格式对象然后指定一个日期格式,接着将日期对象传入日期格式简化方法中获得一个字符串形式的日期接着将其打印就可以获得了
length();可以获取文件大小
File[] listFiles();方法会返回一个抽象路径名数组,并且数组里存放该目录下的所有文件的路径,当然,是File形式的,通过与其他方法和foreach循环的联立使用,我们可以获得该目录下的所有文件的文件名或绝对路径
序列化和反序列化
java对象保存在硬盘中过程是将java对象切分开来保存在硬盘中的,这是为了避免java对象太大而进行的处理方式,这个过程就需要使用到ObjectOutputStream方法,也就是对象输出流,这个过程我们称之为序列化
而反之则是将硬盘中切分的java对象文件复原到内存中,这里需要使用到对象输入流ObjectInputStream方法,这个复原的过程我们称之为反序列化
要令对象序列化,必须令对线实现Serializable接口,其是标志性接口,其内部没有任何方法,其主要作用的是令实现其的类加入一个标志给JVM虚拟机看,JVM虚拟机发现其实现了可序列接口之后,会自动在对象内部生成一个序列化版本号,用来区分不同的类
先创建对象的原因是我们序列化的内容是对象,要想要序列化对象当然得先有个对象不是,调用ObjectOutputStream方法并创造对象输入流对象,然后传入一个FileOutputStream字节输出流对象并指定一个路径就完了,接着我们在调用其writeObject();方法就可以实现序列化了
实现反序列化先创建对象输入流并传入先前序列化的students文件,然后调用其readObject();方法就可以进行反序列化了,但要注意其返回的是一个学生对象,我们用Object承接自然没有问题,接着打印这个学生对象就会自动调用其tostring方法,我们可以再控制台正常看到学生信息就说明我们反序列化成功了,事实上我们也真的可以正常看到学生信息
一次序列化多个对象要借助集合,我们先将对象放入至集合中,然后将集合本身序列化就可以了
参与序列化的Arrarylist集合以及集合中的元素User都需要实现Serializable接口,否则会报异常
transient关键字
transient关键字表示游离的,对于有该关键字修饰的变量,其不参与序列化操作的,如果说我们给我们的Student类的String name加上transient关键字修饰,那么最后我们反序列得到的结果会显示name会null,这是因为我们的name的数据并没有参与序列化,因此不会将数据传入到硬盘中导致的,而Student类中又有String name的变量,因此赋予其默认值null
序列化版本号
java虚拟机判断类是否相同的方法有两种,先进行类名上的判断看是否是同一个类,如果类名相同再判断其序列化版本号,如果序列化版本号相同则说明是同一类,反之则是不同的
序列化版本号用于给JVM判断我们的定义的类是否是同一个类的
假设我们对我们原来的学生类进行了代码上的修改,此时我们再进行反序列化的话,那么JVM就会抛出InvalidClassException异常,也就是无效的类异常。
这个异常的产生原因在于我们修改了学生类,JVM会对其进行重新编译,重新编译生成新的字节码文件的同时会产生新的序列版本号,与我们原先保存在硬盘上的序列化版本号是不一样的,这样我们用原来的文件进行反序列化时JVM会寻找对应的序列化版本号的学生类 ,显然它找不到,所以就会抛出这个异常,就是在告诉我们它找不到指定的类
假设有两个人在一个工程里面创造了两个名字完全相同的两个类,这时我们怎么区分?这时就要靠序列化版本号来区分,因为两个序列化版本号不一样,因此JVM虚拟机可以区分这二者
序列化版本号都是自动生成的,这也是有缺陷的,其中最大的缺陷就是我们不能对我们的源代码进行任何的改动,因为只要一改动那么我们的反序列过程就直接废了,但是在实际的开发需求中,我们总是有对同一个类进行修改的时候,那我们应该要怎么办呢?手动生成一个序列版本号可以解决这个问题
只要我们对实现了Serializable接口的类提供一个固定不变的序列化版本号,这样即使这个类的代码修改了,但是版本号仍然不会改变,java虚拟机仍然会认为其实同一个类
IO和Properties文件联合使用
我们在IDEA中创建一个文件,命名为userinfo,随便写入一些键值对数据
Properties是一个Map集合,集合里有key和value
如果我们想要将userinfo里的内容直接写入到Porperties集合里我们该怎么做?
先创建一个字符输入流对象,其实字节输入流运行,将文件传入之后我们创建一个Properties的Map集合,然后调用集合里的load方法将文件传入即可将文件中的值对应传到集合里了,而且如果我们需要变化集合内的值,我们可以直接变换文档里的值就可以了,这样就省去了我们创建集合时往集合里添加元素的许多繁琐异常的方法,只要往文件里写入我们需要的值,然后利用IO流和Properties就可以直接将值传入了,而且可以随时修改
如果要获取文件中的内容的话,就用Properties对象中的getProperty();方法即可,将左值传入,就能够返回正确的=号右边的值了
对于经常改变的数据,我们可以单独写到一个文件中,用程序动态读取它
像类似于上面的将数据内容写入文件中然后在代码里直接读取添加到集合的文件被称为是配置文件,当其内容格式是key=value的时候,我们把这种配置文件叫做属性配置文件
而java规范中有要求,属性配置文件建议以.properties结尾。
且Properties是专门存放属性配置文件内容的一个类
-
在属性配置文件中,如果我们写入的内容的key相同,那么会覆盖value
-
在属性配置文件中,注释的符号是#
-
在属性配置文件中,=符号可以改成:号,但是我们不建议这样做
-
在属性配置文件中,我们建议=号左右不要加任何的空格
Java8新特性具有许多好处
Lambda表达式
光说肯定感受不到,下面来看看实际的例子
ini
public class AppTest
{
@Test
public void test1(){
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("我爱北京天安门");
}
};
r1.run();
System.out.println("*****************************");
Runnable r2 = () -> System.out.println("我爱北京故宫");
r2.run();
//普通写法
Comparator<Integer> com1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1,o2);
}
};
int compare = com1.compare(12, 12);
System.out.println(compare);
System.out.println("*****************************");
//Lambda表达式写法
Comparator<Integer> com2 = ((o1, o2) -> Integer.compare(o1,o2));
int compare1 = com2.compare(32, 21);
System.out.println(compare1);
System.out.println("*****************************");
//方法引用
Comparator<Integer> com3 = (Integer::compare);
int compare3 = com2.compare(32, 21);
System.out.println(compare1);
}
}
可以看到使用Lambda表达式写法是真的能方便非常多,真的太爽了,无论是从代码的编写还是阅读上,Lambda表达式都很舒服
我们这里重点来介绍下第四点,其是这里应该写为函数式接口的实例,只能用于实现我们的接口的实现具体实现中才能使用,而且要求接口要实现的抽象方法只有一个,如果有多个那这玩意就没得用了
而什么是函数式接口呢?其实只要我们的接口只声明了一个抽象方法,那么该接口就被称为函数式接口
具体的内容的写法都写在下面了,我们就不再赘述了
ini
public class AppTest
{
@Test
public void test1(){
//语法格式一: 无参,无返回值
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("我爱北京天安门");
}
};
r1.run();
System.out.println("*****************************");
Runnable r2 = () -> System.out.println("我爱北京故宫");
Runnable r3 = () -> {
System.out.println("我爱北京故宫");
};
r2.run();
r3.run();
}
//语法格式二:Lambda需要一个参数,但是没有返回值
@Test
public void test2(){
Consumer<String> con = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
con.accept("谎言和誓言的区别是什么");
System.out.println("*****************************");
//Lambda表达式的省略大括号的写法,只有一句语法要执行时可省略大括号
Consumer<String> con1 = (String s) -> System.out.println(s);
//Lambda表达式的不省略大括号的写法,只有一句语法要执行时可省略大括号
Consumer<String> con2 = (String s) -> {
System.out.println(s);
};
//方法引用,一些满足条件的特殊情况下Lambda表达式可进一步简化成这样
Consumer<String> con3 = System.out::println;
//语法格式三:数据类型可以省略,因为可由编译器推断得出,称为"类型推断"
Consumer<String> con4 = (s) -> System.out.println(s);
//语法格式四:Lambda 若只需要一个参数,参数的小括号可以省略
Consumer<String> con5 = s -> System.out.println(s);
con1.accept("test1");
con2.accept("test2");
con3.accept("test3");
con4.accept("test4");
con5.accept("test5");
//类型推断举例
List<String> list = new ArrayList<>(); //泛型的新特性,也是类型推断之一
int[] arr = {1,2,3}; //类型推断
}
//语法格式五:Lambda 需要两个或以上的参数,多条执行语句,并且可以有返回值
@Test
public void test6(){
Comparator<Integer> com1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
System.out.println(o1);
System.out.println(o2);
return o1.compareTo(o2);
}
};
System.out.println(com1.compare(12,21));
System.out.println("**************************************");
Comparator<Integer> com2 = (o1, o2) -> {
System.out.println(o1);
System.out.println(o2);
return o1.compareTo(o2);
};
System.out.println(com2.compare(31,21));
System.out.println("**************************************");
//语法格式六: 当 Lambda 体只有一条语句时,return与大括号若有,都可以省略
Comparator<Integer> com3 = (o1, o2) -> o1.compareTo(o2);
//方法引用在Lambda只有一条语句也有返回值时也可以使用
Comparator<Integer> com4 = Integer::compareTo;
System.out.println(com3.compare(31,21));
}
}
最后我们来做个总结
左边的Lambda形参列表的参数类型可以省略(类型推断),如果Lambda形参列表只有一个参数,其一对()也可以省略
右边的Lambda体应该使用一对{}包裹,如果Lambda体只有一条执行语句(可能是return语句),可以省略这一对{}和return关键字
函数式接口
Java给我们提供了内置的四大核心函数式接口
然后是对其中的两个接口进行举例运用的代码
typescript
public class AppTest
{
//消费型接口Consumer<T> void accept(T t)
@Test
public void test1(){
happyTime(500, new Consumer<Double>() {
@Override
public void accept(Double aDouble) {
System.out.println("捡到了钱"+aDouble);
}
});
System.out.println("********************************");
happyTime(400,money -> System.out.println("捡到了钱"+money));
}
public void happyTime(double money,Consumer<Double> consumer){
consumer.accept(money);
}
//断定型接口 Predicate<T> boolean test(T t)
@Test
public void test2(){
List<String> list = Arrays.asList("北京","南京","东京","西京","普京","吴京");
List<String> filterString = filterString(list, new Predicate<String>() {
@Override
public boolean test(String s) {
return s.contains("京");
}
});
System.out.println(filterString);
System.out.println("**************************************");
List<String> filter = filterString(list,s -> s.contains("京"));
System.out.println(filter);
}
//根据给定的规则,过滤集合中的字符串,此规则由Predicate的方法决定
public List<String> filterString(List<String> list, Predicate<String> predicate){
List<String> filterList = new ArrayList<>();
for (String s : list) {
if(predicate.test(s)){
filterList.add(s);
}
}
return filterList;
}
}
方法引用
arduino
public class AppTest
{
//情况一:对象 :: 实例方法
//Consumer中的void accept(T t)
//PrintStream中的void println(T t)
@Test
public void test1(){
Consumer<String> con1 = str -> System.out.println(str);
con1.accept("test");
System.out.println("************************");
PrintStream ps = System.out;
Consumer<String> con2 = ps::println;
con2.accept("test2");
System.out.println("************************");
Consumer<String> con3 = System.out::println;
con2.accept("test2");
}
//Supplier中的T get()
//Employee中的String getName()
@Test
public void test2() {
Employee emp = new Employee(1001,"Tom",23,5600);
Supplier<String> sup1 = () -> emp.getName();
System.out.println(sup1.get());
System.out.println("*****************************");
Supplier<String> sup2 = emp::getName;
System.out.println(sup2.get());
}
//情况二:类 :: 静态方法
//Comparator中的int compare(T t1,T t2)
//Integer中的int compare(T t1,T t2)
@Test
public void test3() {
Comparator<Integer> com1 = (t1,t2) -> Integer.compare(t1,t2);
System.out.println(com1.compare(12,21));
System.out.println("***********************************");
Comparator<Integer> com2 = Integer::compare;
System.out.println(com2.compare(12,21));
}
//Function中的R apply(T t)
//Math中的Long round(Double d)
@Test
public void test4() {
Function<Double,Long> func = new Function<Double, Long>() {
@Override
public Long apply(Double aDouble) {
return Math.round(aDouble);
}
};
System.out.println(func.apply(12.9));
System.out.println("******************************");
Function<Double,Long> func1 = d -> Math.round(d);
System.out.println(func1.apply(12.3));
System.out.println("******************************");
Function<Double,Long> func2 = Math::round;
System.out.println(func1.apply(12.6));
}
//情况三:类 :: 实例方法 (有难度)
//Comparator中的int compare(T t1,T t2)
//String中的int t1.compareTo(t2)
@Test
public void test5() {
Comparator<String> com1 = (s1,s2) -> s1.compareTo(s2);
System.out.println(com1.compare("abc","abd"));
System.out.println("**********************************");
Comparator<String> com2 = String::compareTo;
System.out.println(com2.compare("abd","abm"));
}
//BiPredicate中的boolean test(T t1,T t2)
//String中的boolean t1.equals(t2)
@Test
public void test6() {
BiPredicate<String,String> pre1 = (s1,s2) -> s1.equals(s2);
System.out.println(pre1.test("abc","abc"));
System.out.println("****************************");
BiPredicate<String,String> pre2 = String::equals;
System.out.println(pre2.test("abc","abc"));
}
//Function中的R apply(T t)
//Employee中的String getName()
@Test
public void test7() {
Employee employee = new Employee(1001, "Jerry", 23, 6000);
Function<Employee,String> func1 = e -> e.getName();
System.out.println(func1.apply(employee));
System.out.println("*******************************");
Function<Employee,String> func2 = Employee::getName;
System.out.println(func2.apply(employee));
}
}
class Employee{
private int id;
private String name;
private int age;
private int salary;
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", name='" + name + ''' +
", age=" + age +
", salary=" + salary +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return id == employee.id && age == employee.age && salary == employee.salary && Objects.equals(name, employee.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name, age, salary);
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
public Employee(int id, String name, int age, int salary) {
this.id = id;
this.name = name;
this.age = age;
this.salary = salary;
}
}
构造器引用与数组引用
arduino
public class AppTest
{
//构造器引用
//Supplier中的T get()
//Employee的空参构造器: Employee()
@Test
public void test1(){
Supplier<Employee> sup = new Supplier<Employee>() {
@Override
public Employee get() {
return new Employee();
}
};
System.out.println(sup.get());
System.out.println("**********************************");
Supplier<Employee> sup1 = () -> new Employee();
System.out.println(sup1.get());
System.out.println("**********************************");
Supplier<Employee> sup2 = Employee::new;
System.out.println(sup2.get());
}
//Function中的R apply(T t)
@Test
public void test2(){
Function<Integer,Employee> func1 = id -> new Employee(id);
Employee employee = func1.apply(1001);
System.out.println(employee);
System.out.println("*******************************");
Function<Integer,Employee> func2 = Employee::new;
Employee employee1 = func2.apply(1001);
System.out.println(employee1);
}
//BiFunction中的R apply(T t,U u)
@Test
public void test3(){
BiFunction<Integer,String,Employee> func1 = (id,name) -> new Employee(id,name);
System.out.println(func1.apply(1001,"Tom"));
System.out.println("*****************************");
BiFunction<Integer,String,Employee> func2 = Employee::new;
System.out.println(func2.apply(1002,"Tom"));
}
//数组引用
//Function中的R apply(T t)
@Test
public void test4(){
Function<Integer,String[]> func1 = length -> new String[length];
String[] apply = func1.apply(5);
System.out.println(Arrays.toString(apply));
System.out.println("*********************************");
Function<Integer,String[]> func2 = String[]::new;
String[] apply2 = func2.apply(5);
System.out.println(Arrays.toString(apply2));
}
}
class Employee{
private int id;
private String name;
private int age;
private int salary;
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", name='" + name + ''' +
", age=" + age +
", salary=" + salary +
'}';
}
public Employee() {
System.out.println("无参构造器被调用");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return id == employee.id && age == employee.age && salary == employee.salary && Objects.equals(name, employee.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name, age, salary);
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
public Employee(int id, String name, int age, int salary) {
this.id = id;
this.name = name;
this.age = age;
this.salary = salary;
}
public Employee(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public Employee(int id, String name) {
this.id = id;
this.name = name;
}
public Employee(int id) {
System.out.println("id形参构造器被调用");
this.id = id;
}
}
最后我们要提一下关于构造器引用,其会自动引用不同的构造方法,但写法都是类名::new的形式,而具体引用什么构造方法取决于我们实现的抽象方法中有什么参数,当然,要保证不出错的前提是我们的内部的确提供了正确且合适的构造方法,连顺序都不能有不对应的地方才行,反之,如果没有合适的,或者是只有顺序不合适的,那么我们就只能用Lambda表达式来解决问题
简单来说,越简单的写法,可以应用的地方就越少,其灵活性就越小
Stream流
Stream API
那么最后我们来做一个总结
Stream的实例化
接着我们先来学习Stream的创建,Stream的创建首先可以通过集合来创建
首先我们定义了一个获得对应集合的静态方法和员工类
csharp
class Employee{
private int id;
private String name;
private int age;
private double salary;
public Employee(int id, String name, int age, double salary) {
this.id = id;
this.name = name;
this.age = age;
this.salary = salary;
}
public static List<Employee> getEmployees(){
List<Employee> list = new ArrayList<>();
list.add(new Employee(1001,"马化腾",34,6000.38));
list.add(new Employee(1002,"马云",12,9876.12));
list.add(new Employee(1003,"刘强东",33,3000.82));
list.add(new Employee(1004,"雷军",26,7657.37));
list.add(new Employee(1005,"李彦宏",65,5555.32));
list.add(new Employee(1006,"比尔盖茨",42,9500.43));
list.add(new Employee(1007,"任正非",26,4333.32));
list.add(new Employee(1008,"扎克伯格",35,2500.32));
return list;
}
}
然后我们来正式学习如何通过集合来创建Stream流
通过集合实例化
下面是演示代码
ini
//创建 Stream方式一:通过集合
@Test
public void test1(){
List<Employee> employees = Employee.getEmployees();
//default Stream<E> stream() : 返回一个顺序流
Stream<Employee> stream = employees.stream();
//default Stream<E> parallelStream() : 返回一个并行流
Stream<Employee> parallelStream = employees.parallelStream();
}
通过数组实例化
ini
//创建 Stream方式二:通过数组
@Test
public void test2() {
int[] arr = {1,2,3,4,5,6};
//调用Arrays类的static <T> Stream<T> stream(T[] array): 返回一个流
IntStream stream = Arrays.stream(arr);
Employee e1 = new Employee(1001, "Tom");
Employee e2 = new Employee(1002, "Jerry");
Employee[] arr1 = {e1,e2};
Stream<Employee> stream1 = Arrays.stream(arr1);
}
Stream.of()
创建代码如下
arduino
//创建 Stream方式三: 通过Steam的of()
@Test
public void test3(){
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
}
无限流
该方式可以创建一个无限流,用得比较少,了解即可
那么我们创建无限流的测试代码如下,这里为了能看到效果我们这里还一并写了停止条件
arduino
//创建 Stream方式四: 创建无限流
@Test
public void test4(){
// 迭代
// public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
//遍历前十个偶数
Stream.iterate(0, t ->t + 2).limit(10).forEach(System.out::println);
// 生成
// public static<T> Stream<T> generate(Supplier<T> s)
Stream.generate(Math::random).limit(10).forEach(System.out::println);
}
第一个0代表的是提供的种子,具体有啥用我也不知道,总是写0总没错,而后面是使用Lambda表达式,本质上是要提供一个接口,然后自己定义需要返回什么对象
Stream流的中间操作
筛选和切片
我们先来学习中间操作的第一部分,筛选和切片,其下提供了四个方法用于中间操作,如下图所示
我们可以写入我们的测试代码如下,其中distinct()元素能发挥作用需要我们集合内存放的元素实现equals方法
csharp
public class AppTest
{
//1-筛选与切片
@Test
public void test1(){
List<Employee> list = Employee.getEmployees();
//filter(Predicate p)--接收 Lambda,从流中排除某些元素
Stream<Employee> stream = list.stream();
//练习:查询员工表中薪资大于7000的员工信息
stream.filter(e->e.getSalary()>7000).forEach(System.out::println);
System.out.println("******************************");
//limit(n)--截断流,使其元素不超过给定数量
//stream.limit(3).forEach(System.out::println);
//上面的代码是会报错的,因为我们的流在结束之后只执行一次,再次调用流会抛出异常
list.stream().limit(3).forEach(System.out::println);
//skip(n) -- 跳过元素,返回一个扔掉了前n个元素的流,若流中元素不足n个,则返回一个空流,与limit(n)截断流互补
System.out.println("******************************");
list.stream().skip(3).forEach(System.out::println);
//distinct()--筛选,通过流所生成元素的hashCode()和equals()去除重复元素
System.out.println("******************************");
list.add(new Employee(1,"test",1,1));
list.add(new Employee(1,"test",1,1));
list.add(new Employee(1,"test",1,1));
list.add(new Employee(1,"test",1,1));
list.add(new Employee(1,"test",1,1));
list.add(new Employee(1,"test",1,1));
list.stream().distinct().forEach(System.out::println);
}
}
映射
下面是示例代码,map方法可以获得接受一个函数作为参数,将函数处理应用到每一个元素上并最终映射成一个新的的元素,具体请看练习1的示例
而flatMap方法可以接受一个函数作为参数,将流中的每个值都换成另外一个流,然后将所有流连接成一个流,我们这里定义了一个传入一个字符串返回一个流的方法,如果我们在上面的字符串集合中用该方法处理流,那么就会获得一个全新的流,该流存放了一个个返回的新流,但是没有自动连接成一个流,因此我们如果要看到里面的效果,就需要在结束方法中取出内部的每一个流再次调用一次结束方法打印流中内容才能看到
而我们的flatMap函数不但能实现同样的效果,还能令其变得更加间接,其下传入对应的返回流的函数,然后其会自动将流连接成一个流,然后我们可以打印该流
这里的区别其实就类似于集合中的add和addall方法,前者直接将整个集合当成一个对象添加,后者会取出其元素添加到集合中(可以这么理解,但本质的运行过程并不完全相同)
当然,我们自己需要提供一个返回Stream流的方法,要不然这个方法都无法启动
scss
//映射
@Test
public void test2(){
//map(Function f)--接受一个函数作为参数,将元素转换为其他形式或提取信息,该函数会被应用到每个元素上,并将其映射形成一个新的元素
List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
list.stream().map(String::toUpperCase).forEach(System.out::println);
System.out.println("****************************************");
//练习1:获取员工姓名长度大于3的员工的名字
List<Employee> employees = Employee.getEmployees();
Stream<String> nameStream = employees.stream().map(Employee::getName);
nameStream.filter(name -> name.length()>3).forEach(System.out::println);
System.out.println("****************************************");
//练习2:
Stream<Stream<Character>> streamStream = list.stream().map(AppTest::fromStringToStream);
streamStream.forEach(s->{
s.forEach(System.out::println);
});
System.out.println("****************************************");
//flatMap(Function f)--接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流
Stream<Character> characterStream = list.stream().flatMap(AppTest::fromStringToStream);
characterStream.forEach(System.out::println);
}
//将字符串中的多个字符构成的集合转换为对应的Stream的实例
public static Stream<Character> fromStringToStream(String str){
List<Character> list = new ArrayList<>();
char[] chars = str.toCharArray();
for (char c : chars) {
list.add(c);
}
return list.stream();
}
//为了便于理解flatMap的作用的类比方法
@Test
public void test3(){
List list1 = new ArrayList();
list1.add(1);
list1.add(2);
list1.add(3);
List list2 = new ArrayList();
list2.add(4);
list2.add(5);
list2.add(6);
list1.add(list2);
System.out.println(list1);
list1.addAll(list2);
System.out.println(list1);
}
}
排序
这一节比较简单,直接看示例代码吧
scss
//3-排序
@Test
public void test4() {
//sorted()--自然排序
List<Integer> list = Arrays.asList(12, 45, 65, 34, 87, 0, -98, 7);
list.stream().sorted().forEach(System.out::println);
//抛出异常,原因是Employee没有是吸纳Comparable接口
// List<Employee> employees = Employee.getEmployees();
// employees.stream().sorted().forEach(System.out::println);
//sorted(Comparator com)--定制排序
List<Employee> employees = Employee.getEmployees();
employees.stream().sorted(Comparator.comparingInt(Employee::getAge)).forEach(System.out::println);
}
Stream流终止操作
匹配和查找
我们先来学习匹配与查找,给我们提供了五个方法
都是比较简单的内容,直接看下面的示例代码即可
scss
//1-匹配与查找
@Test
public void test1(){
List<Employee> employees = Employee.getEmployees();
//allMatch(Predicate p)--检查是否匹配所有元素
//练习:是否所有的员工年龄都大于18
boolean allMatch = employees.stream().allMatch(e -> e.getAge() > 18);
System.out.println(allMatch);
//anyMatch(Predicate p)--检查是否至少匹配一个元素
//练习:是否存在员工的工资大于10000
boolean anyMatch = employees.stream().anyMatch(e -> e.getSalary() > 10000);
System.out.println(allMatch);
//noneMatch(Predicate p)--检查是否没有匹配的元素
//练习:是否存在员工姓"雷"
boolean noneMatch = employees.stream().noneMatch(e -> e.getName().contains("雷"));
System.out.println(noneMatch);
//findFirst--返回第一个元素
Optional<Employee> employee = employees.stream().findFirst();
System.out.println(employee);
//findAny--返回当前流中的任意元素
Optional<Employee> employee1 = employees.stream().findAny();
System.out.println(employee1);
//count--返回流中元素的总个数
long count = employees.stream().filter(e -> e.getSalary() > 5000).count();
System.out.println(count);
//max(Comparator c)--返回流中最大值
//练习:返回最高的工资
Stream<Double> salaryStream = employees.stream().map(e -> e.getSalary());
Optional<Double> max = salaryStream.max(Double::compare);
System.out.println(max);
//min(Comparator c)--返回流中最小值
//练习:返回最低工资的员工
Optional<Employee> employee2 = employees.stream().min(Comparator.comparingDouble(Employee::getSalary));
System.out.println(employee2);
//forEach(Consumer c)--内部迭代
employees.stream().forEach(System.out::println);
//使用集合遍历操作,因为我们这里连流都没创建
employees.forEach(System.out::println);
}
归约
scss
//归约
@Test
public void test3() {
//reduce(T identity, BinaryOperator)--可以将流中元素反复结合起来,得到一个值,返回T
//练习1:计算1-10的自然数的和
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Integer sum = list.stream().reduce(0, Integer::sum);
System.out.println(sum);
//reduce(BinaryOperator)--可以将流中元素反复结合起来,得到一个值。返回Optional<T>
//练习2:计算所有员工工资的总和
List<Employee> employees = Employee.getEmployees();
Stream<Double> salaryStream = employees.stream().map(Employee::getSalary);
Optional<Double> reduce = salaryStream.reduce(Double::sum);
System.out.println(reduce);
}
当然,这里出现了Optional类,这是我们后面要学习的内容,我们现在可以将其简单理解为一个容器,一个简单的装载数据的容器类即可
收集
最后我们来看Stream的终止操作的最后一个,也就是收集
scss
//3-收集
@Test
public void test4() {
//collect(Collector c)--将流转换为其他形式,接收一个Collector接口的实现,用于给Stream中元素汇总的方法
//练习1:查找工资大于6000的员工,结果返回一个List或Set
List<Employee> employees = Employee.getEmployees();
List<Employee> collect = employees.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toList());
collect.forEach(System.out::println);
System.out.println("************************************************");
Set<Employee> set = employees.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toSet());
set.forEach(System.out::println);
}
查阅该网址可以获得关于Stream流的原理解答:www.cnblogs.com/xiaoxiongca...
Optional类
ini
/**
* 为了在程序中避免出现空指针异常而创建的类
*/
public class OptionalTest {
/**
* Optional.of(T t) : 创建一个 Optional 实例,t必须非空
* Optional.empty() : 创建一个空的Optional实例
* Optional.ofNullable(T t) : t可以为null
*/
@Test
public void test(){
Girl girl = new Girl();
girl = null;
//of(T t):保证t是非空的,若不是则会抛出异常
Optional<Girl> optionalGirl = Optional.of(girl);
}
@Test
public void test2(){
Girl girl = new Girl();
girl = null;
//Optional.ofNullable(T t) : t可以为null
Optional<Girl> optionalGirl = Optional.ofNullable(girl);
System.out.println(optionalGirl);
//orElse(T t):如果当前的Optional内部封装的t是非空的,则返回内部的t
//如果内部的t是空的,则返回orElse()方法中的参数t1
Girl girl1 = optionalGirl.orElse(new Girl("赵丽颖"));
System.out.println(girl1);
}
//优化之前的方法,会报空指针异常
public String getGirlName(Boy boy){
return boy.getGirl().getName();
}
@Test
public void test3(){
Boy boy = new Boy();
String girlName = getGirlName(boy);
System.out.println(girlName);
}
//优化之后的方法
public String getGirlName1(Boy boy){
if(boy!=null){
Girl girl = boy.getGirl();
if(girl!=null){
return girl.getName();
}
}
return null;
}
@Test
public void test4(){
Boy boy = new Boy();
boy=null;
String girlName = getGirlName1(boy);
System.out.println(girlName);
}
//使用Optional类的getGirlName()
public String getGirlName2(Boy boy){
Optional<Boy> boyOptional = Optional.ofNullable(boy);
//此时的boy1一定非空
Boy boy1 = boyOptional.orElse(new Boy(new Girl("迪丽热巴")));
Girl girl = boy1.getGirl();
Optional<Girl> girlOptional = Optional.ofNullable(girl);
//girl1一定非空
Girl girl1 = girlOptional.orElse(new Girl("古力娜扎"));
return girl1.getName();
}
@Test
public void test5() {
Boy boy = null;
boy = new Boy();
boy = new Boy(new Girl("苍井空"));
String girlName = getGirlName(boy);
System.out.println(girlName);
}
}
抽象类与接口
abstract抽象类
类之间的共同点抽象之后形成的更高级的类就是抽象类,抽象类无法实例化
抽象类也属于引用数据类型,其次是抽象类的语法是
[修饰符列表] abstract class 类名{ 类体 }
抽象类没有无法实例化但有构造方法,抽象类存在的意义就是为了被继承的
那么final和abstract不能连用,因为被final修饰的类是不能被继承的,如果我们用final去修饰抽象类,那抽象类就不能被继承了
同理abstract和private也是不能连用
抽象类的子类可以是类,也可以使抽象类
如果我们在一个abstract类里只写了一个有参数的构造方法让子类(默认子类不做增改)去继承这个抽象类,由于子类的构造方法里是默认有super()语句的,super语句是调用无参数构造方法的,而在abstract类里,我们由于已经给了一个有参数的构造方法了,那么默认的无参数构造器就没了,那么super();就没有办法找到对应的方法,所以会报错
解决方法有两个,一是直接删除这个有参数的构造方法,二是手动写一个无参数的构造器,这也是为什么我们常说写任何一个构造方法前都应该先手动写一个无参数构造器
抽象方法
抽象方法是只能存在于抽象类当中的方法,抽象方法的特点是带有abstract并且在方法名的()之后直接加分号,比如public abstract void dosome();
相当于是一个普通的实例方法加了一个abstract
抽象类里可以有抽象方法,也可以有非抽象方法
抽象方法只能出现在抽象类中,非抽象类不能出现
非抽象类继承子类必须将抽象方法实现,抽象类中可以使用多态机制
利用多态机制让我们调用抽象类中的方法,就叫做面向抽象编程。面向抽象编程是符合OCP原则的,我们程序员也主要是要用面向抽象的思想来进行编程的,因为面向抽象编程能降低程序的耦合度,提高程序的扩展力
OCP即是开闭原则(Open Closed Principle)简称 OCP
接口
接口是一种"引用数据类型",编译之后也是一个class字节码文件
接口是完全抽象的,而抽象类是半抽象的,或者我们也可以说接口是特殊的抽象类
定义接口的语法是[修饰符列表] interface 接口名{}
接口类支持多继承其他接口类,使用规则是在extends后面用,连接各个接口
非接口类可以同时实现多个接口类,但是必须要在自己的方法体里对接口里的方法都进行实现,该机制弥补了java中类与类之间只能单继承的缺陷
接口中只包含两个内容,一是常量,二是抽象方法,且接口中的元素都是公开的
在interface接口类里定义方法可以省略掉前面的public abstract,直接写抽象方法的名字加上小括号和分号即可
接口类中的变量都是常量,前面的public final修饰符被省略,常量的命名规范是全部大写
接口本身可以当做一个类,可以用类名.的方式调用类里的常量和方法
一个类实现一个接口,必须实现接口内的所有抽象方法
接口与其他非接口类中的继承,我们称之为实现,即implements,其实质作用和继承是一样的
接口类可以和多态联合使用,我们称之为是面向接口编程
extends和implements同时可以出现,使用规则是先继承后实现
接口在开发中的作用
接口在开发中的作用其实类似于多态在开发中的作用
抽象类是半抽象的,而接口是完全抽象的
我们讲解多态的时候讲过,我们要面向抽象编程,不要面向具体对象编程,其实这句话在以后就改成,我们要面向接口编程
因为接口可插拔,可插拔就表示程序的扩展力很强,耦合度很低,这是符合OCP原则的,也是我们程序员应该做出来的程序
类型与类型的关系
抽象类和接口比较,往往是接口使用得比较多,抽象类使用得比较少
Object类其下的方法
Object类是所有类的超级父类,所有类都默认继承了Object类
toStirng()
sun公司设计toString方法的目的是通过调用这个方法可以将一个"java对象"转换成"字符串"的形式,sun公司建议所有的子类都去重写toString方法,同时重写的toString方法应该是简单的,详实的,易阅读的,实际情况中我们可以使用idea的工具来快速重写toString方法
如果我们只输出引用,那么会自动调用该引用里的toString方法
如果toString方法没有重写,其默认输出的当前对象的内存地址
equals()
equals方法默认比较的两个对象的内存地址,其设计目的是为了用于判断两个对象是否相等
推荐每一个类中都重写该方法,可以使用idea的工具帮助重写
Stirng类以及其他Java类库中提供给我们的类,其都已经事先实现好了这种需要实现的方法
我们平时写代码的时候,最好就把每一个equals方法都进行重写,防止我们在以后我们需要调用equals方法的时候,产生不必要的错误
instanceof简单记忆法
想要重写那个类,那个类的类名就放在后面,传入的什么引用,引用名就放前面。如果是判断引用是不是它自身,那么前面写this,后面写引用名
clone()方法
这个方法的主要作用就是可以克隆一个对象,该克隆又分为深克隆和浅克隆,前者指的是克隆出来的对象以及内部的引用类型都是全新的对象,而后者只有对象是全新的,内部的引用对象还是与克隆前对象还是同一个对象
hashcode()方法
hashCode方法中有native关键字,其不是抽象方法,其没有方法体,底层调用的是C++程序
hashcode方法会返回对象的经过了哈希算法之后的内存地址,我们也可以简单把它返回的值理解为是另一种形式的表现的对象的内存地址
内部类
内部内一共有四个,分别是静态内部类、实例内部类、局部内部类以及匿名内部类
匿名内部类指的是直接在需要对应类的地方实例化一个类,由于该类没有名字,因此无法再次调用,只能在实例化的做作用域中进行一次使用,并不推荐使用
数组及数组常见算法
一维数组
在java语言中的数组是一种应用数据类型,其父类是Object
数组本身是一个容器,可以容纳多个元素,这些元素可以是基本数据类型也可以是引用数据类型,如果数组存储的是java对象的话,实际就相当于是储存了引用,我们可以通过每一个引用去调用对应对象内部的方法或者是访问或者是修改其变量
数组本身是引用数据类型,存放在堆内存里。其变量名所储存的内存地址值是数组第一个变量的内存地址,以其首地址来当做数组的内存地址
数组长度一旦确定就不可变
在java中所有的数组都有length属性,用于获取数组的个数
java中的数组要求数组的元素类型统一,比如int类型数组只能存储int类型变量,不能存储其他类型的变量
一维数组中的元素的内存地址是连续的,同时数组也是一种数据结构
对于引用数据类型的数组而言,里面存放的引用的内存地址是不同的,但是储存这些引用的位置本身也存在内存地址,这些存放的位置的内存地址是连续的
数组检索某个下标的元素时效率极高,但是增删某个元素的效率很低,同时数组不可以存储大数据量,因为在内存中很难找到一块特别大的连续内存空间
mian方法中的String数组
在我们的main方法本身public static void main(String[] args){}其表示的意思是,我们构造了一个静态方法,方法名为mian,该方法要接受一个String数组作为参数,数组名为args
既然有方法,那就要被人调用,实际上这个方法是被JVM调用的,JVM调用这个方法的时候,会自动传一个String数组过来
编写这种代码可以在控制台中输出args的长度,会得到0的结果
这个0表示数组是实际存在的,但是里面什么东西都没有放,本身也没有位置给别人放东西,可以理解为存在,但是没什么意义
那么这个数组有什么用呢?其实这个数组是留给用户用的
用户可以在控制台上输入参数,然后这个参数会自动转换成数组,JVM会将这个数组创建出来并传给String[] args
那么我们在idea里要怎么在控制台上输入参数呢?
我们只要点击窗口上的Run,在选中Edit Configuration...在左侧选中我们需要输入参数的类,然后在右侧里输入我们想输入的字符串,空格代表输入停止,空格之后输入的会存放在后一个元素的位置里
二维数组
二维数组是特殊的一维数组,相当于是一个一维数组内每个位置存放着一个数组
二维数组的定义方法是数据类型 数据名,静态初始化的方式是{{},{},{},.....}
数组中的常见方法
System.arraycopy()
使用该方法可以实现数组扩容,其本质是通过拷贝进行的,是把一个数组的内容一个个拷贝到更大的数组里去,然后再销毁原来的数组,是一种效率极低的方式,能不用就尽量不用
该方法同时还可以用来实现数组拷贝
这个源码本身是一个名为arraycopy的静态native的含参方法,这种含参方法需要传入五个参数才能调用
第一个参数是要传入我们要拷贝的数组的内存地址
第二个参数是传入我们要拷贝的数组的起始位置的下标
第三个参数是要传入我们要将数组黏贴到那里的数组的内存地址
第四个参数是传入我们要将数组黏贴到哪里的数组的起始位置的下标
第五个参数是要传入我们要拷贝的数组的长度
源码里的数组的引用类型是Object,以此来避免类型转换异常
Arrays.toString()
使用该方法可以直接打印数组内的值,包括一维数组和二维数组,但是只限于包装类中的数组内容
数组的常见算法
有二分法、冒泡排序、选择排序等,在Arrays工具类中有提供对应的排序和二分方法供给我们使用
二分法的使用必须在排序之后,不然是没有意义的
String类
字符串的存储原理
String属于引用数据类型,不属于基本数据类型,在JAVA中规定用""号括起来的都是String对象,不能再次更改,因为其源码中String内的属性是被final修饰的,因此其不可更改
String类构建的字符串其底层也是一个char数组,该数组被final修饰,因此其无法再进行更改,如果想要一个我们的所需要的不同的字符串,那我们就只能创造新的
String类的变量都是储存在方法区的字符串常量池当中的
ini
String s1 = "abcdef";
String s2 = "abcdef"+"xy";
上面代码JVM图加载的情况是其会先在方法区内存的字符串常量池中创建一个空间用于存放变量"abcdef",而s1则会创建在栈内存的主方法所在的空间里,并储存指向常量池变量"abcdef"的引用
而s2则表示其会在方法区内存中多创建一个储存字符串"xy"的空间,只不过这个空间没有引用去指向它,接着会继续在常量池中创建一个空间用于储存"abcdefxy",然后在栈内存中s2的引用会指向它,这里由于"abcdef"已经有了,所以就不会再次创建了
注意在方法区里的用于储存字符串的空间也是对象,既然是对象,那么就可以调用对应的方法
如果我们采用String a=new String("Hello");String b=new String("Hello");这种形式来创建字符串,其表示的意思是在方法区内存的常量池里同样创造了一块空间用于存放相同的字符串,但是因为我们用这种代码创造了对象,所以实际上我们堆内存里创建一个对象,这个对象存放的内存地址就指向常量池的字符串空间,但是a是属于栈内空间的存放指向堆内存的对象的内存地址,由于new了两个对象,他们在堆内存的空间必然不一样,此时用==号比较时就必然返回false
String类中的方法
String(byte[] bytes)
该方法可以将byte数组传进该方法中,其会将对应的byte的值转换为char类型组合并构造一个全新的String变量
使用该方法需要传入一个byte数组
String(byte[] bytes, int offset, int length)
该方法可以将byte数组里的特定长度的字符取下来将对应的byte值转换成char类型组合并生成一个新的String类型的字符串,使用该方法时要传入一个byte数组,以及我们想要取的位置的下标以及长度
trim()
调用这个方法可以去除字符串前面和后面的空格,中间的不作处理
replace()
该方法可以用于替换字符串里的字符,调用此方法时要输入想要替换的字符串和替换该字符串的内容
getBytes()
将字符串对象转换为一个byte数组对象
equalsIgnoreCase
判断两个字符串是否相等,比较时忽略大小写
StringBuffer类
如果使用+号进行字符串的拼接,会给方法区内存较大的压力,用StringBuffer里的append方法进行拼接比较好
StringBuffer的底层其实是一个byte[]数组,如果我们往里面存放字符串,实际上是存放到byte数组里了,其初始化的容量是16,如果我们进行拼接的时候其字符串的内存大小已经超过16了,那么其会在进行拼接之前先自动进行扩容操作,为了提高性能,可以在创建的时候就给定一个适合的稍微大一点的初始化数组
StringBuffer类底层的char[]数组没有被final修饰,它里面所代表的字符串可以先赋值到一个新的更大的数组里去,然后将原本指向旧的内存地址的引用改成新的,然后旧的数组就被垃圾回收器回收了,通过这种方法达到字符串的拼接,同时省下空间
StringBuilder和StringBuffer都可以完成对字符串的拼接,前者是线程不安全的,后者是线程安全的
Java对日期的处理
Date
java中有专门为我们设置好的一个类让我们能够获得当前的日期与时间,其是位于java.until包下的Date类
Date可以传入毫秒数来构造Date对象,传入对应的毫秒数进去到这个构造方法里,这个构造方法本身会将这个毫秒数读取然后返回一个Date类型的引用,打印这个Date类型的引用可以输入对应的时间,后面也可以接日期格式化的对应代码的应用
该可以用来获取昨天的或者是对应哪天的时间,比如我想要获得昨天的现在的时间,那么我只需要调用这个方法,在括号里传入1970到现在的总毫秒数再减去一天的毫秒数就可以了,同理也是可以获得一年的
SimpleDateFormat
Date里已经重写过toString方法,因此打印的时候会返回一个对人类读者有意义的值,但是因为这是歪果仁搞得东西,所以实际产生的格式仍然不便于我们中国人的阅读
java.text包下的SimpleDateFormat类专门负责日期的格式变换
如果我们想要将一个String类型的日期给转换成Date类型,首先创建对应的字符串,然后创建对应的格式变换对象,调用该对象中的parse方法传入对应字符串即可获得所需的日期对象,此处需要进行异常处理
System.currentTimeMillis()
该静态方法能够让我们获取自1970年1月1日00.00到当前系统时间的总毫秒数,我们可以用这个方法来确定一个方法运行的所需的时间
随机数类Random
随机数Random类存在于java.util包下,可以用于生成随机数
使用时要先new一个Random类的对象出来,然后调用里面对应的方法就可以了
调用方法时如果不传入参数会随机生成该中基本类型的范围内的随机数之一,如果限定了范围则会生成0~length-1的数,其中length代表我们输入的值
枚举类型enum
枚举类型可以自定义构建各种我们所需要的情况的表示方式
语法是enum 枚举名{枚举名词,枚举名词...}
包装类
java中为8中基本数据类型又对应准备了8中包装类型,这8中包装类型都是引用数据类型,包装类内部会提供一些方法供给程序员使用
在Interger类里,是定义了静态代码块的,这个静态代码块会在类加载的时候现在方法区里开辟一个空间,称之为常量池,这个常量池中存储着有-127到127的整数,也就是byte类的取值范围,当我们定义的值在这个区间时,引用就不会再堆内存中开辟空间了,而是会直接指向常量池
为什么要先在方法区内存中开辟这样一个空间呢?这是因为这一段数字使用得真的是太频繁了,所以我们预先开辟出来,这样就可以提高效率
如果我们的数字在常量池之外,则会在堆内存中创建新的对象