首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
80 阅读
2
nvim番外之将配置的插件管理器更新为lazy
58 阅读
3
2018总结与2019规划
54 阅读
4
PDF标准详解(五)——图形状态
33 阅读
5
为 MariaDB 配置远程访问权限
30 阅读
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
Java
emacs
linux
文本编辑器
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
308
篇文章
累计收到
27
条评论
首页
栏目
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
页面
归档
友情链接
关于
搜索到
25
篇与
的结果
2019-07-21
Java 文件操作
java文件操作主要封装在Java.io.File中,而文件读写一般采用的是流的方式,Java流封装在 java.io 包中。Java中流可以理解为一个有序的字符序列,从一端导向到另一端。建立了一个流就好似在两个容器中建立了一个通道,数据就可以从一个容器流到另一个容器文件操作Java文件操作使用 java.io.File 类进行。该类中常见方法和属性有:static String pathSeparator: 多个路径间的分隔符,这个分隔符常用于系统的path环境变量中。Linux中采用 : Windows中采用 ;static String separator: 系统路径中各级目录的分隔符,比如Windows路劲 c:\windows\ 采用的分隔符为 \, 而Linux中 /root 路径下的 分隔符为 /为了达到跨平台的效果,在写路径时一般不会写死,而是使用上述几个静态变量来进行字符串的拼接构造方法有:File(String pathname); 传入一个路径的字符串File(String parent, String child); 传入父目录和子目录的路径,系统会自动进行路径拼接为一个完整的路径File(File parent, String child); 传入父目录的File对象和子目录的路径,生成一个新的File对象常见方法:以can开头的几个方法,用于判断文件的相关权限,比如可读、可写、可执行String getAbsolutePath() 获取文件绝对路径的字符串String getPath() 获取文件的路径,这个方法会根据构造时传入的路径来决定返回绝对路径或者相对路径String getName() 获取文件或者路径的名称long length() 返回文件的大小,以字节为单位,目录会返回0;boolean exists(); 判断文件或者目录是否存在boolean isDirectory(); 判断对应的File对象是否为目录boolean isFile(); 判断对应的File对象是否为文件boolean delete(); 删除对应的文件或者目录boolean mkdir(); 创建目录boolean mkdirs(); 递归创建目录String[] list(); 遍历目录,将目录中所有文件路径字符串放入到数组中File[] listFiles(); 遍历目录,将目录中所有文件和目录对应的File对象保存到数组中返回下面是一个遍历目录中文件的例子public static void ResverFile(String path){ File f = new File(path); ResverFile_Core(f); } public static void ResverFile_Core(File f){ //System.out.println("开始遍历目录:" + f.getAbsolutePath()); File[] subFile = f.listFiles(); for(File sub : subFile){ if(sub.isDirectory()){ if(".".equals(sub.getName()) || "..".equals(sub.getName())){ continue; } ResverFile_Core(sub); }else{ System.out.println(sub.getAbsolutePath()); } } }上述代码根据传入的路径,递归遍历路径下所有文件。从 JDK文档中可以看到 list 和listFiles方法都可以传入一个FileFilter 或者FilenameFilter 的过滤器, 查看一下这两个过滤器:public interface FilenameFilter{ boolean accept(File dir, String name); } public interface FileFilter{ boolean accept(File pathname); }上述接口都是用来进行过滤的,FilenameFilter 会传入一个目录的File对象和对应文件的名称,我们在实现时可以根据这两个值来判断文件是否是需要遍历的,如果返回true则结果会包含在返回的数组中,false则会舍去结果将上述的代码做一些改变,该成遍历所有.java 的文件public static void ResverFile(String path){ File f = new File(path); ResverFile_Core(f); } public static void ResverFile_Core(File f){ //System.out.println("开始遍历目录:" + f.getAbsolutePath()); File[] subFile = f.listFiles(pathname->pathname.isDirectory() || pathname.getName().toLowerCase().endsWith(".java")); for(File sub : subFile){ if(sub.isDirectory()){ if(".".equals(sub.getName()) || "..".equals(sub.getName())){ continue; } ResverFile_Core(sub); }else{ System.out.println(sub.getAbsolutePath()); } } }IO 流Java将所有IO操作都封装在了 java.io 包中,java中流分为字符流(Reader、Writer)和字节流(InputStream、OutputStream), 它们的结构如下:字节流读写文件在读写任意文件时都可以使用字节流进行,文件字节流是 FileInputStream和FileOutputStream//可以使用路径作为构造方式 //FileInputStream fi = new FileInputStream("c:/test.dat"); //可以使用File对象进行构造 FileInputStream fi = new FileInputStream(new File("c:/test.dat")); int i = fi.read(); byte[] buffer = new byte[1024]; while(fi.read(buffer) > 0 ){ //do something } fi.close();下面是一个copy文件的例子public static void CopyFile() throws IOException{ FileInputStream fis = new FileInputStream("e:\\党的先进性学习.avi"); FileOutputStream fos = new FileOutputStream("党的先进性副本学习.avi"); int len = 0; byte[] buff = new byte[1024]; long start = System.currentTimeMillis(); while((len = fis.read(buff)) > 0){ fos.write(buff, 0, len); } long end = System.currentTimeMillis(); System.out.println("耗时:" + (end - start)); fos.close(); fis.close(); }字符流读写文件一般在读写文本文件时,为了读取到字符串,使用的是文件的字符流进行读写。文件字节流是FileReader和FileWriterFileReader fr = new FileReader(new File("c:/test.dat")); char[] buffer = new char[] while(fr.read(buffer) > 0 ){ //do something } fr.close();下面是一个拷贝文本文件的例子public static void CopyFile() throws IOException{ FileReader fr = new FileInputStream("e:\\党的先进性学习.txt"); FileWriter fw = new FileOutputStream("党的先进性副本学习.txt"); int len = 0; char[] buff = new char[1024]; long start = System.currentTimeMillis(); while((len = fr.read(buff)) > 0){ fw.write(buff, 0, len); } long end = System.currentTimeMillis(); System.out.println("耗时:" + (end - start)); fr.close(); fw.close(); }读写IO流的其他操作IO流不仅能够读写磁盘文件,在Linux的哲学中,一切皆文件。根据这点IO流是可以读写任意设备的。比如控制台;之前在读取控制台输入的时候使用的是Scanner,这里也可以使用InputStream或者InputStreamReader。Java中定义了用于控制台输入输出的InputStream 和 OutputStream 对象: System.in 和 System.out//多次读取单个字符 char c; InputStreamReader isr = new InputStreamReader(System.in); System.out.println("输入字符, 按下 'q' 键退出。"); // 读取字符 do { c = (char) isr.read(); System.out.println(c); } while (c != 'q'); isr.close(); //读取字符串 // 使用 System.in 创建 BufferedReader BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String str; System.out.println("Enter lines of text."); System.out.println("Enter 'end' to quit."); do { str = br.readLine(); System.out.println(str); } while (!str.equals("end")); br.close();控制台的写入与读取类似OutputStreamWriter ow = new OutputStreamWriter(System.out); char[] buffer = new char{'a', 'b', 'c'}; ow.write(buffer); ow.flush(); ow.close();由于write函数的功能有限,所以在打印时经常使用的是 System.out.println 函数。缓冲流在操作系统中提到内存的速度是超过磁盘的,在使用流进行读写操作时,CPU向磁盘下达了读写命令后会长时间等待,影响程序效率。而缓冲流在调用write和read方法时并没有真正的进行IO操作。而是将数据缓存在一个缓冲中,当缓冲满后或者显式调用flush 后一次性进行读写操作,从而减少了IO操作的次数,提高了效率。常用的缓冲流有下面几个BufferedInputStreamBufferedOutputStreamBufferReaderBufferWriter分别对应字节流和字符流的缓冲流。它们需要传入对应的Stream 或者Reader对象。下面是一个使用缓冲流进行文件拷贝的例子,与上面不使用缓冲流的拷贝进行对比,当文件越大,效率提升越明显BufferedInputStream bis = new BufferedInputStream(new FileInputStream("E:\\test.avi")); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("test.avi")); int len = 0; byte[] buff = new byte[1024]; long start = System.currentTimeMillis(); while((len = bis.read(buff)) > 0){ bos.write(buff, 0, len); } long end = System.currentTimeMillis(); System.out.println("耗时:" + (end - start)); bos.close(); bis.close();文件编码转换在读取文件时经常出现乱码的情况,乱码出现的原因是文件编码与读取时的解码方式不一样,特别是出现中文的情况。上面说过Java 中主要有字符流和字节流。从底层上来说,在读取文件时都是二进制的数据。然后将二进制数据转化为字符串。也就是先有InputStream/OutputStream 读出二进制数据,然后根据默认的编码规则将二进制数据转化为字符也就是 Reader/Writer。如果读取时的编码方式与文件的编码方式不同,则会出现乱码。我们在程序中使用 InputStreamReader和 OutputStreamWriter 来设置输入输出流的编码方式。//以UTF-8方式写文件 FileOutputStream fos = new FileOutputStream("test.txt"); OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8"); osw.write(FileContent); osw.flush(); //以UTF-8方式读文件 FileInputStream fis = new FileInputStream("test.txt"); InputStreamReader isr = new InputStreamReader(fis, "UTF-8"); BufferedReader br = new BufferedReader(isr); String line = null; while ((line = br.readLine()) != null) { FileContent += line; }序列化与反序列化在程序中经常需要保存类的数据,如果直接使用OutputStream 也是可以保存类数据的,但是需要考虑类中有引用的情况,如果里面有引用,需要保存引用所对应的那块内存。每个类都需要额外提供一个方法来处理存在引用成员的情况。针对这种需求,Java提供了序列化与反序列化的功能Java序列化与反序列化可以使用ObjectOutputStream 和 ObjectInputStream。public class Student{ public String name; public int age; public Date birthday; }比如我们要序列化 上述的 Student 类,可以使用下面的代码ObjectOutputStream oos = ObjectOutputStream(new FileOutputStream("student.dat")); Student stu = new Student(); stu.name = "Tom"; stu.age = 22; stu.brithday = new Date(); oos.writeObject(stu);当然如果要进行序列化和反序列化操作,必须要在类中实现Serializable接口, 这个接口没有任何方法它仅仅作为一个标志,拥有这个标志的方式才能进行序列化。也就是得将上述的Student 类做一个修改public class Student implements Serializable{ public String name; public int age; public Date birthday; }类的静态变量在类的对象创建之前就加载到了内存中。它与具体的类对象无关,所以在序列化时不会序列化静态成员。如果有的成员不想被序列化,可以将它变为静态成员;但是从设计上来说,也不是所有的类成员都可以变为静态成员。为了保证非静态成员可以不被序列化,可以使用 transient 关键字实现了serialiable 接口的类在保存为.class文件 时会增加 一个SerializableID, 序列化时会在对应文件中保存序列号,如果类发生了修改而没有进行序列化操作时,二者不同会抛出一个异常。例如说上述的Student类中先进行了一次序列化,在文件中保存了一个ID,后来根据需求又增加了一个 id 字段,在编译后又生成了一个ID,如果这个时候用之前的文件来反序列化,此时就会报错。为了解决上述问题,可以采用以下几种方法:改类代码文件后重新序列化。增加一个 static final long serialVerssionID = xxxx; 这个ID是之前序列化文件保存的ID。这个操作是为了让新修改的类ID与文件中的ID相同。调用 writeObject 方法时一个文件只能保存一个对象的内容。为了使一个文件保存多个对象,可以使用集合保存多个对象,在序列化时序列化 这个集合
2019年07月21日
5 阅读
0 评论
0 点赞
2019-07-14
Java lambda 表达式
在写Java代码的时候,如果某个地方需要一个接口的实现类,一般的做法是新定义一个实现类,并重写接口中的方法,在需要使用的时候new一个实现类对象使用,为了一个简单的接口或者说为了一个回调函数就得额外编写一个新类,即使说使用匿名内部类来实现,这样似乎比较麻烦。C中的做法是直接传入一个函数指针,而Java中就需要上述麻烦的操作,能不能简单点呢?为此Java中引入了一个lambda表达式的功能。lambda 表达式简介看看之前线程的例子:public class ThreadDemo{ public static void main(String[] args){ //使用匿名内部类的方式 Runnable thread1 = new Runnable(){ @Override public void run(){ System.out.println("当前线程:" + Thread.currentThread().getName() + "正在运行"); } } new Thread(thread1).start(); new Thread(thread1).start(); } }上面使用了匿名内部类的方式来简化了书写。使用lambda之后,可以写的更加简单public class ThreadDemo{ public static void main(String[] args){ //使用匿名内部类的方式 new Thread(()->{ System.out.println("当前线程:" + Thread.currentThread().getName() + "正在运行"); }); } }相比于之前使用匿名内部类的例子,lambda表达式更加关注的是函数实现的功能,而不再关注使用哪个类来实现。写法上更加的简洁。lambda 表达式的基本格式为 (参数列表)->{函数体}; JDK 会根据使用的接口自动创建对应的接口实现类并创建对象。也就是说,这里我们虽然简写了,但是底层仍然是需要通过创建实现类的对象来执行。上述的代码,JVM在执行时根据 Thread类 构造的情况,自动推导出此时应该需要一个Runnable的实现类,并且将lambda表达式中的函数体作为重写接口方法的函数体。需要注意使用lambda表达式的一些约束条件:lambda表达式只能用于重写接口类中的抽象方法。接口中应该只有一个抽象方法。当然上述的代码可以进一步简写。lambda表达式中凡是可以根据定义推导出来的东西就可以省略不写,例如:括号中参数列表中,参数类型可以不写。这个可以根据接口中方法的定义知道需要传哪些类型的参数括号中参数只有一个,那么类型和括号都可以省略如果函数体中代码只有一行,那么不管它是否有返回值,return和大括号以及语句末尾的分号可以都省略(注意,这里需要都省略)根据这些简写的规则,上述代码可以进一步简化public class ThreadDemo{ public static void main(String[] args){ //使用匿名内部类的方式 new Thread(()->System.out.println("当前线程:" + Thread.currentThread().getName() + "正在运行")); } }函数式接口上面说到,lambda表达式的条件是需要接口中只有一个抽象方法。像这种接口也被叫做是函数式接口。可以使用注解 @FunctionalInterface 来标明定义了一个函数式接口。在 java.util.function 包中提供了一些函数式接口。Supplier 生产者接口,它的定义如下@FunctionalInterface public interface Supplier<T>{ T get(); }这个接口的get方法可以产生一个结果供外部程序使用。Consumer,消费者接口需要传入一个结果供其处理,它的定义如下:@FunctionalInterface public interface Consumer<T>{ void accept(T t); }Predicate:判断的接口,根据给定的值返回True或者False@FunctionalInterface public interface Predicate<T>{ default Predicate<T> and(Predicate<? super T> other); //返回一个组合的谓词,表示该谓词与另一个谓词的短路逻辑AND。 static <T> Predicate<T> isEqual(Object targetRef); //返回根据 Objects.equals(Object, Object)测试两个参数是否相等的 谓词 。 default Predicate<T> negate(); //返回表示此谓词的逻辑否定的谓词。 default Predicate<T> or(Predicate<? super T> other); //返回一个组合的谓词,表示该谓词与另一个谓词的短路逻辑或。 boolean test(T t); //在给定的参数上评估这个谓词。 }Function 接口:接受一个参数并产生结果的函数@FunctionalInterface public interface Function<T,R>{ default <V> Function<T,V> andThen(Function<? super R,? extends V> after); //返回一个组合函数,首先将该函数应用于其输入,然后将 after函数应用于结果。 default <V> Function<V,R> compose(Function<? super V,? extends T> before); //返回一个组合函数,首先将 before函数应用于其输入,然后将此函数应用于结果。 static <T> Function<T,T> identity(); //返回一个总是返回其输入参数的函数。 R apply(T t); //接收指定参数处理并返回一个处理结果 }方法引用方法引用通过方法的名字来指向一个方法。方法引用可以使语言的构造更紧凑简洁,减少冗余代码。它主要用来针对lambda表达式做进一步的优化方法引用需要保证被引用的方法已经存在。方法引用使用一对冒号来表示 ::List names = new ArrayList(); names.add("Google"); names.add("Runoob"); names.add("Taobao"); names.add("Baidu"); names.add("Sina"); names.forEach(t->System.out.println(t));上述代码是采用lambda表达式的写法,接下来采用方法引用的方式,进一步简化代码List names = new ArrayList(); names.add("Google"); names.add("Runoob"); names.add("Taobao"); names.add("Baidu"); names.add("Sina"); names.forEach(System.out::println);方法引用的常见方式有:通过对象名引用对象方法通过类名引用静态方法通过this关键字,引用本类的成员方法通过构造函数引用:类名::newclass Car { public static Car create(final Supplier<Car> supplier) { return supplier.get(); } public static void collide(final Car car) { System.out.println("Collided " + car.toString()); } public void follow(final Car another) { System.out.println("Following the " + another.toString()); } public void repair() { System.out.println("Repaired " + this.toString()); } } //使用构造函数的引用 final Car car = Car.create( Car::new ); final List< Car > cars = Arrays.asList( car ); //静态方法的引用 cars.forEach( Car::collide ); //
2019年07月14日
2 阅读
0 评论
0 点赞
2019-07-07
Java 多线程
Java内部提供了针对多线程的支持,线程是CPU执行的最小单位,在多核CPU中使用多线程,能够做到多个任务并行执行,提高效率。使用多线程的方法创建Thread类的子类,并重写run方法,在需要启动线程的时候调用类的start() 方法,每一个子类对象只能调用一次start()方法,如果需要启动多个线程执行同一个任务就需要创建多个线程对象实现Runnable 接口并重写run 方法,传入这个接口的实现类构造一个Thread类,然后调用Thread类的start方法实现Callable 接口并重写call方法,然后使用Future 来包装 Callable 对象,使用 Future 对象构造一个Thread对象,并调用Thread 类的start方法启动线程平时在使用上第一个方式用的很少,一般根据情况使用第2中或者第3中,与第一种方式相比,它们具有的优势如下:降低了程序的耦合性,它将设置线程任务和开启线程进行了分离避免了单继承的局限性,一旦继承了Thread 类,那么他就不能继承其他类。使用重写接口的方式可以再继承一个别的类第3中方式相比第二种方式来说,它提供了一个获取线程返回值的方式。我们在call函数中返回值,通过 FutureTask 对象的get方法来获取返回值public class ThreadTask extends Thread{ @Override public void run(){ System.out.println("当前线程:" + getName() + "正在运行"); } } public class ThreadDemo{ public static void main(String[] args){ new ThreadTask().start(); new ThreadTask().start(); } }public class ThreadDemo{ public static void main(String[] args){ //使用匿名内部类的方式 Runnable thread1 = new Runnable(){ @Override public void run(){ System.out.println("当前线程:" + Thread.currentThread().getName() + "正在运行"); } } new Thread(thread1).start(); new Thread(thread1).start(); } }public class ThreadDemo implements Callable<Integer>{ public static void main(String[] args){ FutureTask<Integer> ft = new new FutureTask<>(new ThreadDemo()); new Thread(ft).start(); System.out.println("线程返回值:" + ft.get()); } @Override public Integer call() throws Exception { System.out.println("当前线程:" + Thread.currentThread().getName() + "正在运行"); return 1; } }thread 状态在操作系统原理中讲到,线程有这么几种状态:新建、运行、阻塞、结束。而Java中将线程状态进行了进一步的细分,根据阻塞原因将阻塞状态又分为:等待(调用等待函数主动让出CPU执行权), 阻塞(线程的时间片到达,操作系统进行线程切换)它们之间的状态如下:等待唤醒入上图所示,可以使用wait/sleep方法让线程处于等待状态。在另一个线程中使用wait线程对象的notify方法可以唤醒wait线程。wait/notify 方法定义于 Object 方法,也就是说所以的对象都可以有wait/notify 方法。void wait() ;调用该函数,使线程无限等待,直到有另外的线程将其唤醒 void wait(long timeout);调用该函数,使线程进行等待,直到另外有线程将其唤醒或者等待时间已过 void notify(); 唤醒正在等待对象监视器的单个线程 void notifyAll(); 唤醒正在等待对象监视器的所有线程。上面说过这些方法都是在 Object 类中实现的,也就是说所有的对象都可以调用。上面的等待监视器就是随意一个调用了wait 的对象。这个对象会阻塞它所在的线程。线程同步我们知道在访问多个线程共享的资源时可能会发生数据的安全性问题。因此这个时候需要做线程的同步Java中同步的方法有: 同步代码块、同步方法和Lock锁的机制同步代码块同步代码块是使用synchronized来修饰需要进行同步的代码块同步代码块需要提供一个锁对象,当一个线程执行到这个代码块时,该线程获得锁对象。当另外的线程也执行到同一个锁对象的同步代码块时,由于无法获取到锁对象因此会陷入等待。直到获得锁对象的线程执行完同步代码块,并释放锁。这里获取、释放锁由Java虚拟机自己完成。例如public static void synchronizedCode(){ Runnable thread = new Runnable(){ private int ticket = 100; @Override public void run(){ synchronized (this){ while(ticket > 0){ //这里休眠10s,用来表示提交订单后的付款等操作 try{ Thread.sleep(10); }catch(Exception e){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket); ticket--; } System.out.println(Thread.currentThread().getName() + "票已售罄"); } } }; new Thread(thread).start(); new Thread(thread).start(); new Thread(thread).start(); }同步方法同步方法是使用 synchronized 关键字修饰的方法。它与同步代码块的原理相同,保证了多个线程只有一个处于同步代码块中。同步方法中也有一个锁对象,这个锁对象是this这个对象,静态方法的锁对象是本类的class文件对象。public static void synchronizedMethod(){ Runnable thread = new Runnable(){ private int ticket = 100; @Override public void run(){ payTicket(); } public synchronized void payTicket(){ while(ticket > 0){ //这里休眠10s,用来表示提交订单后的付款等操作 try{ Thread.sleep(10); }catch(Exception e){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket); ticket--; } System.out.println(Thread.currentThread().getName() + "票已售罄"); } }; new Thread(thread).start(); new Thread(thread).start(); new Thread(thread).start(); }Lock 锁机制除了上述方法,可以使用lock锁来进行同步,在执行代码前先调用 lock方法获得锁,执行完成之后使用unlock 来释放锁。例如下列的例子public static void lockMethod(){ Runnable thread = new Runnable(){ private int ticket = 100; private Lock lock = new ReentrantLock(); @Override public void run(){ try{ lock.lock(); while(ticket > 0){ //这里休眠10s,用来表示提交订单后的付款等操作 Thread.sleep(10); System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket); ticket--; } }catch(Exception e){ e.printStackTrace(); }finally{ lock.unlock(); } System.out.println(Thread.currentThread().getName() + "票已售罄"); } }; new Thread(thread).start(); new Thread(thread).start(); new Thread(thread).start(); }
2019年07月07日
5 阅读
0 评论
0 点赞
2019-06-30
Java 异常处理
异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。比如说,你的代码少了一个分号,那么运行出来结果是提示是错误 java.lang.Error;如果你用System.out.println(11/0),那么你是因为你用0做了除数,会抛出 java.lang.ArithmeticException 的异常。Java中的异常主要分为下列几类:检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。所有的异常类是从 java.lang.Exception 类继承的子类。 Exception 类是 Throwable 类的子类。除了Exception类外,Throwable还有一个子类Error 。它们之间的关系入下图:从Exception继承的类都是异常,异常可以被处理,处理完后程序仍然可以继续运行。从Error继承来的类都是错误,在运行时错误无法被处理,只能修改代码逻辑。从Runtime中继承的类都是运行时异常,这类异常在程序中可以处理,也可以不处理。而非运行时异常在代码中必须处理。不然编译会报错。Java中异常处理的方式Java中的异常处理主要有下列几种:使用 throw 在指定方法中抛出指定异常。比如 throw IOException(); 在方法中抛出了一个IO异常使用 throws 将异常抛出给调用者处理。在函数声明时使用。方法声明时可以抛出多个异常,如果多个异常有继承关系,那么只需要抛出父类异常即可。如果父类的方法没有抛出异常,子类在重写父类方法时也不能使用这种方式抛出异常try...catch 处理异常。在使用try 处理异常时需要注意:如果catch 中捕获的有多个异常,且异常间有继承关系,那么必须把子类写在前面,父类在后面异常中的常用方法Throwable 中定义了3个异常处理的方法:String getMessage(): 返回异常的详细信息String toString() : 返回异常的简短信息void printStackTrace(): 打印异常的调用信息这些异常信息一般在try...catch 中使用,例如try { //do something }catch(Exception e){ e.printStackTrace(); }finally 关键字无论异常是否发生,finally中的代码都会执行。一般finally中编写释放资源的代码,比如释放文件对象。需要注意的是,finally中会改变return的执行顺序,不管return在哪,都会最后执行finally中的returntry{ //do some thing return; }catch(Exception e) { return; } finally{ return; //会执行这个 } return;自定义异常类自定义异常时需要注意:异常类都必须继承自 Throwable类,如果要定义检查性异常,需要继承 Exception,要定义运行时异常,需要继承 RuntimeException。class MyException extends Exception{ }假设我们定义一个异常类,表示取钱的异常,当取钱数少于1000时报异常,提示用户去ATM取,可以这样写class TooLittleMoneyException extends Exception { private int money; private String message; TooLittleMoneyException(int money){ message = "" + money + "太少,请到ATM自助取款机去取"; } String getMessage(){ return message; } } //取钱方法 //打开交易通道 //校验账户是否合法 try{ if (money < 1000){ throw TooLittleMoneyException(money); } //取钱,并在对应账户中减少相应的金额 }catch(TooLittleMoneyException e){ System.out.println(e.getMessage()); }finally{ //关闭交易通道 }
2019年06月30日
5 阅读
0 评论
0 点赞
2019-06-23
Java 容器
之前学习了java中从语法到常用类的部分。在编程中有这样一类需求,就是要保存批量的相同数据类型。针对这种需求一般都是使用容器来存储。之前说过Java中的数组,但是数组不能改变长度。Java中提供了另一种存储方式,就是用容器类来处理这种需要动态添加或者删除元素的情况概述Java中最常见的容器有一维和多维。单维容器主要是一个节点上存储一个数据。比如列表和Set。而多维是一个节点有多个数据,例如Map,每个节点上有键和值。单维容器的上层接口是Collection,它根据存储的元素是否为线性又分为两大类 List与Set。它们根据实现不同,List又分为ArrayList和LinkedList;Set下面主要的实现类有TreeSet、HashSet。它们的结构大致如下图:Collection 接口Collection 是单列容器的最上层的抽象接口,它里面定义了所有单列容器都共有的一些方法:boolean add(E e):向容器中添加元素void clear(): 清空容器boolean contains(Object o): 判断容器中是否存在对应元素boolean isEmpty(): 容器是否为空boolean remove(Object o): 移除指定元素<T> T[] toArray(T[] a) : 转化为指定类型的数组Listlist是Collection 中的一个有序容器,它里面存储的元素都是按照一定顺序排序的,可以使用索引进行遍历。允许元素重复出现,它的实现中有 ArrayList和 LinkedListArrayList 底层是一个可变长度的数组,它具有数组的查询快,增删慢的特点LinkedList 底层是一个链表,它具有链表的增删快而查询慢的特点SetSet集合是Collection下的另一个抽象结构,Set类似于数学概念上的集合,不关心元素的顺序,不能存储重复元素。TreeSet是一颗树,它拥有树形结构的相关特定HashSet: 为了加快查询速度,它的底层是一个hash表和链表。但是从JDK1.8以后,为了进一步加快具有相同hash值的元素的查询,底层改为hash表 + 链表 + 红黑树的结构。相同hash值的元素个数不超过8个的采用链表存储,超过8个之后采用红黑树存储。它的结构类似于下图的结构在存储元素的时候,首先计算它的hash值,根据hash值,在数组中查找,如果没有,则在数组对应位置存储hash值,并在数组对应位置添加元素的节点。如果有,则先判断对应位置是否有相同的元素,如果有则直接抛弃否则在数组对应位置下方的链表或者红黑树中添加节点。从上面的描述看,想要在HashSet中添加元素,需要首先计算hash值,在判断集合中是否存在元素。这样在存储自定义类型的元素的时候,需要保证类能够正确计算hash值以及进行类型的相等性判断。因此要重写类的hashCode和equals 方法。例如下面的例子class Person{ private String name; private int age; Person(){ } Person(String name, int age){ this.name = name; this.age = age; } public int getAge(){ return this.age; } public String getName(){ return this.name; } public void setAge(int age){ this.age = age; } public void setName(String name){ this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); } @Override public int hashCode() { return Objects.hash(this.name, this.age); } }上面说到HashSet是无序的结构,如果我们想要使用hashSet,但是又想它有序,该怎么办?在Set中提供了另一个实现,LinkedHashMap。它的底层是一个Hash表和一个链表,Hash表用来存储真正的数据,而链表用来存储元素的顺序,这样就结合了二者的优先。MapMap是一个双列的容器,一个节点存储了两个值,一个是元素的键,另一个是值。其中Key 和 Value既可以是相同类型的值,也可以是不同类型的值。Key和Value是一一对应的关系。一个key只能对应一个值,但是多个key可以指向同一个value,有点像数学中函数的自变量和值的关系。Map常用的实现类有: HashMap和LinkedHashMap。常用的方法有:void clear(): 清空集合boolean containsKey(Object key): map中是否包含对应的键V get(Object key): 根据键返回对应的值V put(K key, V value): 添加键值对boolean isEmpty(): 集合是否为空int size(): 包含键值对的个数遍历针对列表类型的,元素顺序固定,我们可以使用循环依据索引进行遍历,比如for(int i = 0; i < list.size(); i++){ String s = list.get(i); }而对于Set这种不关心元素的顺序的集合来说,不能再使用索引了。针对单列集合,有一个迭代器接口,使用迭代器可以实现遍历迭代器迭代器可以理解为指向集合中某一个元素的指针。使用迭代器可以操作元素本身,也可以根据当前元素寻找到下一个元素,它的常用方法有:boolean hasNext() : 当前迭代器指向的位置是否有下一个元素E next(): 获取下一个元素并返回。调用这个方法后,迭代器指向的位置发生改变使用迭代器的一般步骤如下:使用集合的 iterator() 返回一个迭代器循环调用迭代器的 hasNext方法,判断集合中是否还有元素需要遍历使用 next方法,找到迭代器指向的下一个元素//假设set是一个 HashSet<String>集合 Iterator<String> it = set.iterator(); while(it.hasNext()){ Stirng s = it.next(); }Map遍历索引和迭代器的方式只能遍历单列集合,像Map这样的多列集合不能使用上述方式,它有额外的方法,主要有两种方式获取key的一个集合,遍历key集合并通过get方法获取value获取键值对组成的一个集合,遍历这个新集合来得到键值对的值针对第一种方法,Map中有一个 keySet() 方法。这个方法会获取到所有的key值并保存将这些值保存为一个新的Set返回,我们只要遍历这个Set并调用 Map的get方法即可获取到对应的Value, 例如:// 假设map 是一个 HashMap<String, String> 集合 Set<String> kSet = map.keySet(); Iterator<String> key = kSet.iterator(); while(it.hasNext()){ String key = it.next(); String value = map.get(key); }针对第二种方法,可以先调用 Map的 entrySet() 获取一个Entry结构的Set集合。Entry 中保存了一个键和它对应的值。使用结构中的 getKey() 和 getValue() 分别获取key和value。这个结构是定义在Map中的内部类,因此在使用的时候需要使用Map这个类名调用// 假设map 是一个 HashMap<String, String> 集合 Set<Map.Entry<String,String>> entry = map.entrySet(); Iterator<Map.Entry<String, String>> it = entry.iterator(); while(it.hasNext()){ Map.Entry<String, String> me = it.next(); String key = me.getKey(); String value = me.getValue(); }for each 循环在上述遍历的代码中,不管是使用for或者while都显得比较麻烦,我们能像 Python 等脚本语言那样,直接在 for 中使用迭代吗?从JDK1.5 以后引入了for each写法,使Java能够直接使用for迭代,而不用手工使用迭代器来进行迭代。for (T t: set); 上述是它的简单写法。例如我们对遍历Set的写法进行简化//假设set是一个 HashSet<String>集合 for(String s: set){ //TODO:do some thing }我们说使用 for each写法主要是为了简化迭代的写法,它在底层仍然采用的是迭代器的方式来遍历,针对向Map这样无法直接使用迭代的结构来说,自然无法使用这种简化的写法,针对Map来说需要使用上述的两种遍历方式中的一种,先转化为可迭代的结构,然后使用for each循环// 假设map 是一个 HashMap<String, String> 集合 Set<Map.Entry<String, String>> set = map.entrySet(); for(Map.Entry<String, String> entry: set){ String key = entry.getKey(); String value = entry.getValue(); System.out.println(key + "-->" + value); }泛型在上述的集合中,我们已经使用了泛型。泛型与C++ 中的模板基本类似,都是为了重复使用代码而产生的一种语法。由于这些集合在创建,增删改查上代码基本类似,只是事先不知道要存储的数据的类型。如果没有泛型,我们需要将所有类型对应的这些结构的代码都重复写一遍。有了泛型我们就能更加专注于算法的实现,而不用考虑具体的数据类型。在定义泛型的时候,只需要使用 <>中包含表示泛型的字母即可。常见的泛型有:T 表示TypeE 表示 Element<> 中可以使用任意标识符来表示泛型,只要符合Java的命名规则即可。使用 T 或者 E 只是为了方便而已,比如下面的例子public static <Element> void print(Element e){ System.out.println(e); }当然也可以使用Object 对象来实现泛型的重用代码的功效,在对元素进行操作的时候主要使用java的多态来实现。但是使用多态的一个缺点是无法使用元素对象的特有方法。泛型的使用泛型可以在类、接口、方法中使用在定义类时定义的泛型可以在类的任意位置使用class DataCollection<T>{ private T data; public T getData(){ return this.data; } public void SetData(T data){ this.data = data; } }在定义类的时候定义的泛型在创建对象的时候指定具体的类型.也可以在定义接口的时候定义泛型public interface DataCollection<T>{ public abstract T getData(); public abstract void setData(T data); }定义接口时定义的泛型可以在定义实现类的时候指定泛型,或者在创建实现类的对象时指定泛型public class StringDataCollectionImpl implements DataCollection<String>{ private String data; public String getData(){ return this.data; } public void SetData(String data){ this.data = data; } } public interface DataCollection<T> implements DataCollection<T>{ private T data; public T getData(){ return this.data; } public void SetData(T data){ this.data = data; } }除了在定义类和接口时使用外,还可以在定义方法的时候使用,针对这种情况,不需要显示的指定使用哪种类型,由于接收返回数据和传入参数的时候已经知道了public static <Element> Element print(Element e){ System.out.println(e); return e; } String s = print("hello world");泛型的通配符在使用通配符的时候可能有这样的需求:我想要使用泛型,但是不希望它传入任意类型的值,我只想要处理继承自某一个类的类型,就比如说我只想保存那些实现了某个接口的类。我们当然可以将数据类型定义为某个接口,但是由于多态的这一个缺陷,实现起来总不是那么完美。这个时候可以使用泛型的通配符。泛型中使用 ? 作为统配符。在通配符中可以使用 super 或者 extends 表示泛型必须是某个类型的父类或者是某个类型的实现类class Fruit{ } class Apple extends Fruit{ } class Bananal extends Fruit{ } static void putFruit(<? extends Fruit> data){ }上述代码中 putFruit 函数中只允许 传递 Fruit 类的子类或者它本身作为参数。当然也可以使用 <? super T> 表示只能取 T类型的父类或者T类型本身。
2019年06月23日
3 阅读
0 评论
0 点赞
2019-06-16
Java 常用类
之前将Java的大部分语法都回顾完了,后面添加一些常见的操作,基础语法就结束了。至于在这里再次提到常用类是由于有一部分体现在使用它的继承类或者接口之类的。这些需要有面向对象编程的基础Object类Object类是所有类的基类,只要定义了类,即使没有显式的声明继承自Object类,也会从该类继承。这个类没有什么是需要显式调用的,很多东西都需要重写类的方法来达到相关效果,比如常用的两个方法:String toString() : 将类转化为字符串。一般来说直接打印新定义的类都会打印出对象的地址值,如果需要打印里面的相关值,需要重写toString方法boolean equals(Object obj): 一般来说,== 比较的是对象的地址值,而针对字符串或者其他对象可能需要根据别的值来比较是否相等,这个时候需要重写这个方法。protected Object clone(): 在C++中如果直接使用内存拷贝来拷贝对象的话,如果对象中有指针变量,可能会导致一系列的问题,这种拷贝方法叫做浅拷贝。这个方法用来执行深度拷贝操作。public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }上述代码是Object类的 toString 方法,从代码上看,默认会调用对象的hashCode 方法获取hash值,并转化为字符串。时间操作Date 类Date 类用来处理时间,它能精确到毫秒Date(): 获取当前时刻的Unix时间戳Date(long date): 根据一个时间戳初始化一个对象long getTime(): 获取当前对象对应的时间戳这个方法目前只是用来进行这些操作,后续其他时间的格式化,获取对应的年、月、日操作在后续的JDK版本都用另外的类来进行SimpleDateFormat 类上面提到,Date类只用来进行时间戳相关转化的操作,而具体与时间相关的操作都用这个类来进行。该类继承自 DataFormat 类, DataFormat是一个抽象类。SimpleDateFormat() : 使用默认的模式来格式化Date类SimpleDateFormat(String pattern): 按照指定格式来格式化Date类SimpleDateFormat(String pattern, Locale locale): 构造一个 SimpleDateFormat使用给定的模式和给定的区域设置的默认日期格式符号模式类似于格式化字符串,与常见语言的格式化时间的方式类似。String format(Date date): 按照构造时提供的模式来将传入的date 对象格式化为字符串Date parse(String source): 从给定字符串的开始解析文本以生成日期Calendar 类上述两个与时间相关的类已经解决了时间的获取以及格式化输出的操作。但是关于时间还需要进行年、月、日相关的操作。比如加一年、减一个月等等。跟具体年月相关的操作使用 Calendar类。这个类也是一个抽象类,但是可以使用它的相关静态方法来创建对象static Calendar getInstance(): 使用默认时区和区域设置获取日历。static Calendar getInstance(Locale aLocale): 使用默认时区和指定的区域设置获取日历。常见的方法如下:int get(int field): 返回指定字段的日历值void set(int year, int month, int date, int hourOfDay, int minute, int second): 设置字段中的值 YEAR , MONTH , DAY_OF_MONTH , HOUR_OF_DAY , MINUTE和 SECONDabstract void add(int field, int amount): 根据日历的规则,将指定的时间量添加或减去给定的日历字段Date getTime(): 根据日历对象返回一个对应的Date类void setTime(Date date): 将Date类转化为日历类这些get和set方法有的需要一个值表示需要修改日历中的哪个值。比如 YEAR表示年、MONTH表示月、DAY_OF_MONTH表示月中的天数、HOUR_OF_DAY表示小时等等从这3个类的相关操作来说,Date类作为沟通其他两个类的桥梁,常见的策略是: DateFormat -->Date -->Calender 或者 Calender-->Date-->DateFormat下面是一个简单的例子DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date date = df.parse("2018-10-1 12:12:12"); Calendar c = Calendar.getInstance(); c.set(Calendar.YEAR, 2019); date = c.getTime(); System.out.println("Time:" + df.format(date));System 类这个类用于获取系统相关的内容,里面都是一些静态方法。常用的方法有:static long currentTimeMillis(): 返回当前系统时间,以毫秒为单位static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length): 数组复制方法StringBuilder 类我们说String方法中的字符不能修改,如果要修改,必须重新分配一个String,并舍弃原来的String方法。当String操作过多,频繁的分配回收,影响程序效率。StringBuilder类与String相比,可以修改里面的字符值。相比String来说效率更高StringBuilder(): 构造一个能容纳16个字符的字符容器StringBuilder(int capacity) :构造一个容纳指定字符的容器StringBuilder(String str): 构造一个初始化为指定字符串内容的字符串构建器。append方法:它有一系列的重载方法。往字符容器中添加指定的内容toString(): 转化为String 对象包装类Java中针对 普通类型都有一个对应的类,封装了一系列的操作,比如int 类的包装类是 Integer, double 类的是 Double等等。一般来说,将对应的基本类型转化为包装类的过程称为装箱;将包装类转化为基本类型的过程称为拆箱,从JDK1.5之后支持自动装箱和自动拆箱,比如Double d = 1.5; int i = Integer(10); int i1 = Integer("10");
2019年06月16日
5 阅读
0 评论
0 点赞
2019-06-09
Java 匿名对象与内部类
一般在编写代码时可能会遇到这样的场景——在某些时候,我需要定义并某个类,但是只会使用这一次,或者是某个类对象只会使用一次,为它们专门取名可能会显的很麻烦。为了应对这种情况,Java中允许使用匿名对象和匿名内部类的方式来解决这个矛盾匿名对象普通的类对象在使用时会定义一个类类型的变量,用来保存new出来的类所在的地址。而匿名类取消掉了这个变量,这个地址由编译器来处理,并且在new出来之后,它占用的内存会有JVM自动回收掉。后续无法再使用了。例如public class Student{ public void classBegin(){ System.out.println("good morning teacher!"); } } new Student().classBegin();匿名对象最常用的方式是作为函数的参数,比如上述的打印语句 "good morning teacher!" 它就是一个匿名对象,由于字符串是以对象的形式存储的,所以这里实际上就是一个没有使用对象引用的匿名对象。当然也可以将匿名对象作为函数的返回值。内部类内部类的种类:成员内部类、静态内部类、局部内部类、匿名内部类成员内部类java中允许在一个类中定义另一个类。例如public class Car{ public class Engine{ } }上述例子在Car这个类中定义了一个Engine类,那么Car就是外部类,而Engine就是内部类。使用内部类需要注意:外部类是包含内部类的,所以内部类可以看到外部类的所有属性和方法,包括private方法。但是反过来则不行;使用内部类主要有两种方式:在外部类中使用内部类的成员(间接使用)。这种方法一般是在外部类的方法中创建内部类的对象,并调用对象的方法直接使用:根据上面的定义,可以这样使用 `Car.Engine eng = new Car().new Engine()比如下面的例子public class Car{ public class Engine{ public void start(){ System.out.println("引擎启动"); } } //间接调用 public void start(){ System.out.println("打火"); new Engine().start(); } public static void main(String[] args){ new Car().start(); //直接调用 Car.Engine engine = new Car().new Engine(); engine.start(); } }当外部类和内部类的成员发生命名冲突的时候在内部类中可以使用 外部类.this.成员变量 来访问外部类的成员比如说public class Car{ public String type = "奥迪"; public class Engine{ public String type = "奥迪引擎"; public void start(){ System.out.println("引擎启动"); } public void carType(){ System.out.println(Car.this.type); } } //间接调用 public void start(){ System.out.println("打火"); new Engine().start(); } public static void main(String[] args){ Car car = new Car(); //直接调用 Car.Engine engine = new Car().new Engine(); engine.start(); engine.carType(); } }局部内部类内部类不光可以直接定义在外部类中作为成员内部类,也可以定义在方法中,作为局部内部类局部内部类也叫区域内嵌类,局部内部类与成员内部类类似,不过,区域内嵌类是定义在一个方法中的内嵌类主要特定有:局部内部类只能在对应方法中访问,在方法外无效不能使用private,protected,public修饰符。不能包含静态成员局部内部类如果想要访问方法中的局部变量时,局部变量必须是常量。因为局部变量时分配在栈中,而局部内部类是分配在堆中的,有可能出现这样的情况,外部类的方法执行完了,内存被回收了,但是局部内部类可能还在,所以在访问局部变量时,做了一个拷贝将局部变量拷贝到局部内部类所在的堆中。为了保证数据的完整性,所以这里被拷贝的变量不允许再做修改。public class carShow(){ public void showCar(){ final float price = 10000000f; final String type = "奔驰"; class Car(){ public void show(){ System.out.println("这个车是" + type + ",售价:" + price); } } } }静态内部类内部类如果使用static声明,则此内部类就称为静态内部类。它可以通过 外部类 . 内部类 的方式来访问。由于静态内部类是与对象无关的,在使用静态类的成员时是不需要创建对象的。所以如果想要在静态内部类中来访问外部类的成员变量,必须通过外部类的对象实例来访问。public class Company { String companyNam; static String country; static class Clear{ String name; public Clear() { } public Clear(String name) { super(); this.name = name; } public void work(String name){ String na = new Company().companyNam="联想"; country="中国"; System.out.println(name+"为"+na+"打扫卫生,该公司属于"+country); } } }匿名内部类如果一个内部类在整个操作中只使用一次的话,就可以定义为匿名内部类。匿名内部类也就是没有名字的内部类,这是java为了方便我们编写程序而设计的一个机制,因为有时候有的内部类只需要创建一个它的对象就可以了,以后再不会用到这个类,这时候使用匿名内部类就比较合适。匿名内部类,一般都伴随着接口一起使用比如public interface USB{ public abstract void open(); public abstract void close(); } public class Demo{ public static void main(String[] args){ USB usb = new USB(){ public void open(){} public void close(){} } usb.open(); usb.close(); //使用匿名内部类的匿名对象的方式 USB usb = new USB(){ public void open(){} public void close(){} }.open(); } }在Demo这个类的main方法中创建了一个局部的内部类,这个内部类没有名字,也就是创建了一个匿名内部类。
2019年06月09日
8 阅读
0 评论
0 点赞
2019-06-01
Java 接口与多态
上一篇说了Java面向对象中的继承关系,在继承中说到:调用对象中的成员变量时,根据引用类型来决定调用谁,而调用成员方法时由于多态的存在,具体调用谁的方法需要根据new出来的对象决定,这篇主要描述的是Java中的多态以及利用多态形成的接口多态当时在学习C++时,要使用多态需要定义函数为virtual,也就是虚函数。类中存在虚函数时,对象会有一个虚函数表的头指针,虚函数表会存储虚函数的地址,在使用父类的指针或者引用来调用方法时会根据虚函数表中的函数地址来调用函数,会形成多态。当时学习C++时对多态有一个非常精炼的定义:基类的指针指向不同的派生类,其行为不同。这里行为不同指的是调用同一个虚函数时,会调用不同的派生类函数。这里我们说形成多态的几个基本条件:1)指针或者引用类型是基类;2)需要指向派生类;3)调用的函数必须是基类重写的函数。public class Parent{ public void sayHelllo(){ System.out.println("Hello Parent"); } public void sayHello(String name){ System.out.println("Hello" + name); } } public class Child extends Parent{ public void sayHello(){ System.out.println("Hello Child"); } }根据上述的继承关系,我们来看下面几个实例代码,分析一下哪些是多态Parent obj = new Child(); obj.sayHello();该实例构成了多态,它满足了多态的三个条件:Parent 类型的 obj 引用指向了 new 出来的Child子类、并且调用了二者共有的方法。Parent obj = new Child(); obj.sayHello("Tom");这个例子没有构成多态,虽然它满足基类的引用指向派生类,但是它调用了父类特有的方法。Parent obj = new Parent(); obj.sayHello();这个例子也不满足多态,它使用父类的引用指向了父类,这里就是一个正常的类方法调用,它会调用父类的方法Child obj = new Child(); obj.sayHello();这个例子也不满足多态,它使用子类的引用指向了子类,这里就是一个正常的类方法调用,它会调用子类的方法那么多态有什么好处呢?引入多态实质上也是为了避免重复的代码,而且程序更具有扩展性,我们通过println函数来说明这个问题。public void println(Object x) { String s = String.valueOf(x); synchronized (this) { print(s); newLine(); } } //Class String public static String valueOf(Object obj) { return (obj == null) ? "null" : obj.toString(); }函数println实现了一个传入Object的重载,该函数调用了String类的静态方法 valueOf, 进一步跟到String类中发现,该方法只是调用了类的 toString 方法,传入的obj可以是任意继承Object的类(在Java中只要是对象就一定是继承自Object),只要类重写了 toString 方法就可以直接打印。这样一个函数就实现了重用,相比于需要后来的人额外重载println函数来说,要方便很多。类类型转化上面的println 函数,它需要传入的是Object类的引用,但是在调用该方法时,从来都没有进行过类型转化,都是直接传的,这里是需要进行类型转化的,在由子类转到父类的时候,Java进行了隐式类型转化。大转小一定是安全的(这里的大转小是对象的内存包含关系),子类一定可以包含父类的成员,所以即使转化为父类也不存在问题。而父类引用指向的内存不一定就是包含了子类成员,所以小转大不安全。为什么要进行小转大呢?虽然多态给了我们很大的方便,但是多态最大的问题就是父类引用无法看到子类的成员,也就是无法使用子类中的成员。这个时候如果要使用子类的成员就必须进行小转大的操作。之前说过小转大不安全,由于父类可能有多个实现类,我们无法确定传进来的参数就是我们需要的子类的对象,所以java引入了一个关键字 instanceof 来判断是否可以进行安全的转化,只要传进来的对象引用是目标类的对象或者父类对象它就会返回true,比如下面的例子Object obj = "hello" System.out.println(obj instanceof String); //true System.out.println(obj instanceof Object); //true System.out.println(obj instanceof StringBuffer); //false System.out.println(obj instanceof CharSequence); //true抽象方法和抽象类我们说有了多态可以使代码重用性更高。但是某些时候我们针对几个有共性的类,抽象出了更高层面的基类,但是发现基类虽然有一些共性的内容,但是有些共有的方法不知道如何实现,比如说教科书上经常举例的动物类,由于不知道具体的动物是什么,所以也无法判断该动物是食草还是食肉。所以一般将动物的 eat 定义为抽象方法,拥有抽象方法的类一定必须是抽象基类。抽象方法是不需要写实现的方法,它只需提供一个函数的原型。而抽象类不能创建实例,必须有派生类重写抽象方法。为什么抽象类不能创建对象呢?对象调用方法本质上是根据函数表找到函数对应代码所在的内存地址,而抽象方法是未实现的方法,自然就无法给出方法的地址了,如果创建了对象,而我的对象又想调用这个抽象方法那不就冲突了吗。所以规定无法实例化抽象类。抽象方法的定义使用关键字 abstract,例如public abstract class Life{ public abstract void happy(); } public class Cat{ public void happy(){ System.out.println("猫吃鱼"); } } public class Cat{ public void happy(){ System.out.println("狗吃肉"); } } public class Altman{ public void happy(){ System.out.println("奥特曼打小怪兽"); } }上面定义了一个抽象类Life 代表世间的生物,你要问生物的幸福是什么,可能没有人给你答案,不同的生物有不同的回答,但是具体到同一种生物,可能就有答案了,这里简单的给出了答案:幸福就是猫吃鱼狗吃肉奥特曼爱打小怪兽。使用抽象类需要注意下面几点:不能直接创建抽象类的对象,必须使用实现类来创建对象实现类必须实现抽象类的所有抽象方法,否则该实现类也必须是抽象类抽象类可以有自己的构造方法,该方法仅供子类构造时使用抽象类可以没有抽象方法,但是有抽象方法的一定要是抽象类接口接口就是一套公共的规范标准,只要符合标准就能通用,比如说USB接口,只要一个设备使用了USB接口,那么我的电脑不管你的设备是什么,插上就应该能用。在代码中接口就是多个类的公共规范。Java中接口也是一个引用类型。接口与抽象类非常相似,同样不能创建对象,必须创建实现类的方法。但是接口与抽象类还是有一些不同的。 抽象类也是一个类,它是从底层类中抽象出来的更高层级的类,但是接口一般用来联系多个类,是多个类需要实现的一个共同的标准。是从顶层一层层扩展出来的。接口的一个常见的使用场景就是回调,比如说常见的窗口消息处理函数。这个场景C++中一般使用函数指针,而Java中主要使用接口。接口使用关键字 interface 来定义, 比如public interface USB{ public final String deviceType = "USB"; public abstract void open(); public abstract void close(); }接口中常见的一个成员是抽象方法,抽象方法也是由实现类来实现,注意事项也与之前的抽象类相同。除了有抽象方法,接口中也可以有常量。接口中的抽象方法是没有方法体的,它需要实现类来实现,所以实现类与接口中发生重写现象时会调用实现类,那么常量呢?public class Mouse implements USB{ public final String deviceType = "鼠标"; public void open(){ } public void close(){ } } public class Demo{ public static void main(String[] args){ USB usb = new Mouse(); System.out.println(usb.deviceType); } }常量的调用遵循之前说的重载中的属性成员调用的方式。使用的是什么类型的引用,调用哪个类型中的成员。与抽象类中另一个重要的不同是,接口运行多继承,那么在接口的多继承中是否会出现冲突的问题呢public interface Storage{ public final String deviceType = "存储设备"; public abstract void write(); public abstract void read(); } public class MobileHardDisk implements USB, Storage{ public void open(){ } public void close(){ } public void write(){ } public void read(){ } } public class Demo{ public static void main(String[] args){ MobileHardDisk mhd = new MobileHardDisk(); System.out.println(mhd.deviceType); } }编译上述代码时会发现报错了,提示 USB 中的变量 deviceType 和 Storage 中的变量 deviceType 都匹配 ,也就是说Java中仍然没有完全避免冲突问题。接口中的默认方法有的时候可能会出现这样的情景,当项目完成后,可能客户需求有变,导致接口中可能会添加一个方法,如果使用抽象方法,那么接口所有的实现类都得重复实现某个方法,比如说上述的代码中,USB接口需要添加一个方法通知PC设备我这是什么类型的USB设备,以便操作系统匹配对应的驱动。那么可能USB的实现类都需要添加一个,这样可能会引入大量重复代码,针对这个问题,从Java 8开始引入了默认方法。默认方法为了解决接口升级的问题,接口中新增默认方法时,不用修改之前的实现类。默认方法的使用如下:public interface USB{ public final String deviceType = "USB"; public abstract void open(); public abstract void close(); public default String getType(){ return this.deviceType; } }默认方法同样可以被所有的实现类覆盖重写。接口中的静态方法从Java 8中开始,允许在接口中定义静态方法,静态方法可以使用实现类的对象进行调用,也可以使用接口名直接调用接口中的私有方法从Java 9开始运行在接口中定义私有方法,私有方法可以解决在默认方法中存在大量重复代码的情况。虽然Java为接口中新增了这么多属性和扩展,但是我认为不到万不得已,不要随便乱用这些东西,毕竟接口中应该定义一系列需要实现的标准,而不是自己去实现这些标准。最后总结一下使用接口的一些注意事项:接口没有静态代码块或者构造方法一个类的父类只能是一个,但是类可以实现多个接口如果类实现的多个接口中有重名的默认方法,那么实现类必须重写这个实现方法,不然会出现冲突。如果接口的实现类中没有实现所有的抽象方法,那么这个类必须是抽象类父类与接口中有重名的方法时,优先使用父类的方法,在Java中继承关系优于接口实现关系接口与接口之间是多继承的,如果多个父接口中存在同名的默认方法,子接口中需要重写默认方法,不然会出现冲突final关键字之前提到过final关键字,用来表示常量,也就是无法在程序中改变的量。除了这种用法外,它还有其他的用法修饰类,表示类不能有子类。可以将继承关系理解为改变了这个类,既然final表示常量,不能修改,那么类自然也不能修改修饰方法:被final修饰的方法不能被重写修饰成员变量:表示成员变量是常量,不能被修改修饰局部变量:表示局部变量是常量,在对应作用域内不可被修改
2019年06月01日
4 阅读
0 评论
0 点赞
2019-05-25
Java 继承
之前说过了Java中面向对象的第一个特征——封装,这篇来讲它的第二个特征——继承。一般在程序设计中,继承是为了减少重复代码。继承的基本介绍public class Child extends Parent{ //TODO } 使用继承后,子类中就包含了父类所有内容的一份拷贝。子类与父类重名成员的访问。重名属性当子类与父类中有重名的数据成员时,如果使用子类对象的引用进行调用,那么优先调用子类的成员,如果子类成员中没有找到那么会向上查找找到父类成员。例如public class Parent{ public int num = 10; } public class Child extends Parent{ public int num = 20; public static void main(String[] args){ Parent obj = new Child(); System.out.println(obj.num); //输出10 Child c = new Child(); System.out.println(c.num); //输出20 } }二者同样是创建的Child对象,那么为何一个是10,而一个是20 呢? 在C/C++中经常提到一个概念就是,指针本身存储的是一个地址值,指针本身是没有任何数据类型的,而指针指向的地址有,我们在程序中定义的指针类型决定了代码在访问指针所指向的内存时是如何翻译这段内存的。Java中虽然说没有开放操作内存的能力,但是引用本身也是一个指针。如果将父类类型的引用来保存子类对象的地址,那么从引用类型来看,它只能看到父类的相关内容,所以这里它将对象所在内存当做父类的类型,所以第一个打印语句访问到的是父类的 num 成员。而第二个它能看到子类的整个内存,所以说它优先使用子类的 num 成员。上面是第一种情况,即在外部直接调用,如果通过方法来访问呢public class Parent{ private int num = 10; public int getNum(){ return this.num; } } public class Child extends Parent{ public int num = 20; public int getNum(){ return this.num; } public static void main(String[] args){ Parent obj = new Child(); System.out.println(obj.getNum()); //输出20 Child c = new Child(); System.out.println(c.getNum()); //输出20 } }第一条输出语句实际上使用了Java中的多态特性,这个时候会调用子类的方法,而第二条直接调用子类方法,而在子类方法中通过this调用自身的 num 成员,所以这里会输出两个20不管哪种情况,在通过对象的引用调用对象成员的时候会根据引用类型来翻译对应的内存,找到需要访问的变量,如果子类中未找到则向上查找父类,如果父类也未找到,则会报错。下面来看看Java中继承的内存图来加深这个的理解public class Parent{ public int num = 10; public int getNum(){ return this.num; } } public class Child extends Parent{ public int num = 20; public int getNum(){ return this.num; } public void show(){ int num = 30; System.out.println(num); //30 System.out.println(this.num); //20 System.out.println(super.num); //10 } public static void main(String[] args){ Child obj = new Child(); obj.Show(); obj.getNum(); //20 } }对象的内存分布大致如下:首先JVM加载程序的时候将类定义放到方法区中,此时方法区中有两个类的定义,其中子类中有一个 [[class_super]] 标志,用于标明它的父类是哪个。然后创建main的栈并执行main函数,在main函数中创建了一个Child的对象,那么JVM会根据方法区中的相关内容在堆中创建子类对象, 子类对象中包含一整份的父类的拷贝。然后调用Show方法,它首先根据方法区中的标识查找,在子类中找到了这个方法创建方法栈并调用。在方法中,定义了一个局部变量 num,当访问 num这个变量时会首先在栈中查找,如果找到则访问,如果没有则在子类中查找,如果子类中也没有,则会访问父类。就这样在栈中找到了num。接着访问 this.num。 这个时候JVM会首先到子类中查找,找不到则会进一步查找父类。发现在子类中找到了。接着访问 super.num,那么JVM会主动去父类中查找。show方法执行完了之后,JVM回收它的堆栈,接着调用getNum() 方法,查找方式与调用show的相同。后面的就不再说了,与之前的类似。重写向上面的例子中这样的。当子类与父类的方法名、参数列表相同时,如果用子类对象进行调用,那么会调用子类方法,这个时候父类的同名方法就被重写了。注意:这里并没有强调返回值也一样,其实这里只需要返回值能正常的发生隐式转换即可。这里也没有规定它的访问权限,这里只要求子类方法的访问权限大于等于父类方法的访问权限,也就是在同一条件下保证二者都能访问。虽然没有这两方面的限制,但是一般情况下父类的方法与子类重写的方法在形式上应该是完全一样的。public class Parent{ public int num = 10; public int getNum(int n){ return this.num; } } public class Child extends Parent{ public int num = 20; public int getNum(float f){ return this.num; } }上面的代码并没有实现重写的功能,但是如果程序员自己理解不到为,认为这是一个重写,并且在代码中按照重写在使用,那么编译虽然不会报错,但是在运行过程中可能会出现问题,这个时候可以使用 @Overwrite 注解,来告诉编译器,这里是一个重写,如果编译器判断这个不是重写,那么编译会报错,这样就将运行时错误提升到了编译时,有注意bug的排除。注解与注释有什么相同与区别呢:我认为二者都是用于对程序做一个说明,但是注释是给人看的,注解是给机器看的。重写与重载有什么区别呢?重写是发生在类的继承关系中的,要求函数名、参数列表相同重载并没有规定使用的场合,要求函数名相同、而参数列表不同既然说到重载,我想起来了java与c++中重载的一个区别void sayHello(long f); void sayHello(int);根据上述函数的定义,java和c++中都形成了一个重载的关系,如果在C中使用代码 sayHello(10) 来调用的时候,因为这里的10既可以理解为long也可以理解为int,所以这里发生了二义性,编译会报错,而java中 10 默认为int,所以这里会调用int参数的函数,而想要调用long型的参数,得写成 sayHello(10L)。这算是二者之间和有趣的一个现象。构造函数与C/C++中的相同,调用构造的时候会优先调用父类构造,像上面的代码中,并没有明显的调用构造,在创建类的时候会隐式的调用构造,并在子类构造中隐式调用父类构造。但是一旦手工定义了构造,那么编译器将不再额外提供构造。public class Parent{ Parent(int n){ System.out.println(n); } } public class Child extends Parent{ Child(){ //这个会编译报错 System.out.println("Child"); } }Child构造函数默认会调用父类的无参构造,但是由于父类提供了构造,此时编译器不再提供默认的无参构造,所以这里找不到父类的无参构造,报错。这个代码可以进行如下的修改public class Child extends Parent{ Child(){ //这个会编译报错 super(19); System.out.println("Child"); } //或者 Child(int n){ //隐式调用super(n) System.out.println("Child"); } }这里使用super关键字显示调用父类的带参构造。或者定义一个带参构造,编译器会默认为你添加上调用父类带参构造的过程。在Java中super关键字的作用主要有以下几点:在子类中访问父类成员在子类中调用父类构造函数,这种用法必须保证super在子类构造中是第一条被执行的语句,而且只能有唯一的一条我们说super代表父类,那么this代表的就是类本身,那么它有什么具体的作用,又有哪些需要注意的点呢?this可以访问本类成员变量在本类的成员方法中访问另一个成员方法在本类构造方法中访问另一个构造注意:第3中用法中,this关键字必须是构造方法执行的第一条语句,而且只能有唯一的一条。这条规则与super关键字的相同,那么在构造中既有this又有super的时候该怎么办呢?答案是无法这么使用,Java中规定this和super关键字在构造函数中只能出现一个。Java中的 继承关系在C++中,最让人头疼的是多继承的菱形继承关系,为了防止二义性,Java禁止多继承,只允许单继承。但是java中运行多继承,并且一个父类运行有多个子类。在Java的多级继承中如果出现同名的情况,访问时该怎么办呢?原则仍然相同,根据new出来的对象和左侧保存对象的引用类型来判断,如果是父类类型,则访问成员变量时只能访问父类,如果是访问方法则需要考虑多态。如果创建的是父类则只能访问父类的成员变量和方法
2019年05月25日
3 阅读
0 评论
0 点赞
2019-05-18
java 常见类
上次提前说了java中的面向对象,主要是为了使用这些常见类做打算,毕竟Java中一切都是对象,要使用一些系统提供的功能必须得通过类对象调用方法。其实Java相比于C来说强大的另一个原因是Java中提供了大量可用的标准库字符串字符串可以说是任何程序都离不开的东西,就连一个简单的hello world程序都用到了字符串,当时C语言中对字符串的支持并不太好,C语言中的字符串实质上是一个字符数组。为了方便不同的C/C++库都有自己的字符串实现。而Java中内置了对字符串的支持,Java中的字符串是一个叫做String的对象。根据jdk文档的描述,我们需要注意一下几点:Java程序中的所有字符串文字(例如"abc" )都被实现为此类的实例String对象是可以共享的String对象是不可变的字符串的内存分布一般把类似于 "abc" 这样直接通过字面值表示的字符串作为字面常量,这种在Java中也是一个字符串,只是它与普通的new出来的字符串在内存的存储上有点不一样,下面请看下面的代码class StringDemo{ public static void main(String[] args){ String a = "abc"; String b = "abc"; String c = new String("abc"); System.out.println(a == b); System.out.println(a == c); System.out.println(b == c); } }针对字符串来说 == 比较的是它们的地址是否相同,这个程序分别输出的是 true、false、false,也就是说a b 是指向的同一个地址空间,而c则不是。它们的内存分布如下:一般程序在加载到内存地址空间后,会被划分为4个部分,全局数据段、代码段、堆、栈。而全局代码段是用来存放全局变量的。在C中如果我们写下这样的代码:char* psz1 = "abc"; char* psz2 = "abc";那么在程序加载到内存中时,在全局数据段中会存在一个连续的内存空间保存的是 'a','b','c','\0' 这4个值,一旦有char型指针指向"abc" 这样的字符串,那么系统会自动将这段内存的地址给赋值到对应的指针变量中,而且这个内存是只读内存,如果尝试往里面写入数据,则会造成程序崩溃。Java中也是类似的,当出现 "abc" 的时候,其实系统早就为它在堆中创建了一个String对象,如果去阅读String的源码就会发现String中负责保存字符串的是一个 byte型的数组,所以在初始化的时候会再创建一个byte型数组,然后由字符串中成员变量保存它的地址,所以在内存图中看到有String也有byte[]。而且这个字符串是保存在堆中的常量字符串池中的,它的生命周期与程序相同(或者说与主线程相同)。每当直接使用 "abc" 这样的字面常量的时候会自动将常量字符串池中相关的字符串对象的指针赋值给对应的对象。这样造成了上述程序中 a == b 为true的情况。而c是通过new关键字在程序运行期间动态创建的。所以JVM会在程序执行到这步的时候额外创建一个对象,并将 "abc" 这个字符串对应的byte[] 中的值拷贝到新的内存中。这样就很容易理解上面的前两条了,至于字符串不可变,可以参考我之前写的关于类型中的说明(字符串的值发生改变时,在内存中其实是开辟了一块新的内存用于保存新的字符串内容,而丢弃了从前的字符串)常见字符串方法这里再简单的列举一下字符串中常见的方法,这些方法都可以在JDK文档都可以查到。String(); //初始化新创建的 String对象,使其表示空字符序列 String(byte[] bytes); //通过使用平台的默认字符集解码指定的字节数组来构造新的 String String(byte[] bytes, int offset, int length); //从bytes[] 数组中的第offset 位置开始,截取length个成员来初始化一个String char charAt(int index); //返回指定位置处的索引 int compareTo(String anotherString); // 按字典顺序比较两个字符串的大小,为0表示两个字符串相同 int compareToIgnoreCase(String str); //比较两个字符串的大小,忽略大小写 String concat(String str) ; //字符串拼接 byte[] getBytes(Charset charset); //将字符串转化为byte型数组,并返回新的byte数组 int indexOf(String str); //返回字串第一次出现的位置 int length() ; //返回字符串的长度 String[] split(String regex); //按正则表达式进行分割,并返回对应的字符串数组注意一下,这里返回数组或者新字符串的,都是在函数内部新建的,与原来的无关。所以这里是没办法拿到字符串底层的数组对象再来修改内存值的。数组java中数组的定义如下:int[] Array1 = new int[10]; //定义了一个拥有10个整型数据的数组 int[] Array2 = new int[]{1, 2, 3, 4, 5, 6, 7,8, 9, 0}; //创建数组并初始化 int[] Array3 = {1,2 ,3,4,5,6,7,8,9,0}; 相比于C中数组的定义来说,Java中的定义更容易让人理解,对应数据类型后面加一对 [] 就是对应的数组类型了。而C中,中括号是写在变量后面的,相比于Java中的定义来说就显的有点怪异了。或者说C中从根本上来说数组并不算是一种特别的数据类型,仅仅只是开辟相同数据类型的一块连续的内存而已。至于[] 在C中应该只是表示寻址而已,毕竟汇编中我们经常看到类似于 esp:[eax] 这样的东西。Java中的数组是一种单独的数据类型,它是一种引用类型,也就是说它的变量名中保存的是它的地址。它的使用十分的简单,与C/C++中数组的使用基本相同,注意事项也是基本相同。但是有一点很重要的不同,Java中的数组允许动态指定长度,也就是通过变量来指定长度,而C中必须静态的指定长度,也就是在程序运行之前就需要知道它的长度。这是因为Java中数组是引用类型,是new在堆上的,而C中数组是分配在全局变量区或者栈上的,在程序运行之初就需要为数组分配内存。//这样的代码是可以编译通过的 int length = 10; int array[] = new int[length];数组作为函数参数import java.util.Arrays; class Demo{ public static void main(String[] args){ int length = 10; int array[] = new int[length]; System.out.println(Arrays.toString(array)); test(array); System.out.println(Arrays.toString(array)); } public static void test(int[] array){ for(int i = 0; i < array.length; i++) { array[i] = i; } } }运行上述的代码,发现函数中修改array的值在函数结束后也可以生效,这是因为数组是一个引用类型,在C中我们说要想改变实参的值,需要传入对应的引用或者指针。在函数中通过引用访问,实际上在访问对应的内存,所以这里其实是在修改对应内存的值。当然可以修改实参的值了。ArrayList类之前在数组中,我们说数组一旦定义,是不能改变大小的,那么如果我后续需要使用可变大小的数组呢?Java中提供了ArrayList这样的容器。由于它是一个通用的容器,而java又是一个强类型的语言,所以在定义的时候需要事先指定我们需要使用容器存储何种类型的数据。一般ArrayList的定义如下:ArrayList<String> array = new ArrayList(); 表示容器内部存储的是字符串。需要注意的是容器中只能存储引用类型,不能存储像int、double、char这样的基本类型,如果要存储这样的数据,需要存储它们对应的封装类。比如int 类型对应的封装类为 Integer。它的常用方法如下:ArrayList(); //构造方法 boolean add(E e);//添加元素 void clear(); //清空 E get(int index); //获取指定位置的元素 int indexOf(Object o); //查询元素第一次出现的位置 E remove(int index); //删除指定位置的元素 E remove(Object o); //从列表中删除指定元素的第一个出现(如果存在) int size(); //获取容器中元素个数 void sort(Comparator<? super E> c); //使用提供的 Comparator对此列表进行排序键盘输入Java中的键盘输入主要通过Scanner类来实现,Scanner需要提供一个输入流,从输入流中获取输入。一般常用的输入流是 System.in 表示从键盘输入,例如:Scanner sc = new Scanner(System.in);Scanner类中常用方法是一系列的next方法,next方法主要功能是根据指定 的分割符,从输入流中取出下一个输入并做相应的转化,比如nextInt()会转化为int,nextBoolean() 会转化为boolean类型等等,next()方法会直接转化为字符串。默认情况下next函数会通过空格进行转化。import java.util.Scanner; import java.util.ArrayList; class Demo{ public static void main(String[] args){ ArrayList<String> array = new ArrayList<String>(); Scanner sc = new Scanner(System.in); String str = sc.next(); array.add(str); while(sc.hasNext()){ array.add(sc.next()); } for(int i = 0; i < array.size(); i++) { System.out.println(array.get(i)); } } }这段代码,会将输入的数据依次存储到ArrayList容器中。因为程序事先不知道用户会输入多少数据,所以这里采用可以可变长度的容器来存储//输入(> 表示cmd的提示符) >hello world python java c++ c lisp // 输入ctrl + c来退出sc.next的输入 > ctrl+c //输出 >hello world python java c++ c lisp上述代码首先执行到sc.next位置,并且中断下来,我们输入上述的一些字符串,然后回车,然后程序继续执行,在循环中根据空格,依次从里面取出每一个值,并放到容器中。当没有值时,程序会再次中断在sc.next()的位置,这个时候输入 ctrl + c ,此时程序再次执行到 sc.hasNext() 这个地方会返回false,这个时候循环退出,并依次打印这些内容。这个程序证明了上面说的,next方法会根据指定的分割符,依次从输入流中取出下一个输入。当然如果想要一次读取一行,可以使用 nextLine方法。更多内容请查阅JDK文档。
2019年05月18日
1 阅读
0 评论
0 点赞
1
2
3