• Java基础复习计划(三)


    散碎知识点

    • Math.round() 方法进行四舍五入计算,实现是:Math.floor(a + 0.5f)
      floor : 意为地板,指向下取整,返回不大于它的最大整数
      ceil : 意为天花板,指向上取整,返回不小于它的最小整数
      round : 意为大约,表示“四舍五入”,而四舍五入是往大数方向入.

    • 关于方法区溢出:
      经常动态生成大量 Class 的应用中,Spring、hibernate 对类进行增强的时候使用 CGLib 类字节码技术 ,其他运行在 JVM 的动态语言;
      常见的还有大量 JSP 或动态产生 JSP 文件的应用(JSP 第一次运行时需要编译)

    • public Method[] getDeclaredMethods() 返回类或接口声明的所有方法,包括 public, protected, default (package) 访问和 private 方法的 Method 对象,但不包括继承的方法。当然也包括它所实现接口的方法。
      public Method[] getMethods() 返回类的所有 public 方法,包括其继承类的公用方法,当然也包括它所实现接口的方法。

    • 关于类加载器的简要分类:
      引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的。
      扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。
      系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。
      tomcat 为每个 App 创建一个 Loader,里面保存着此 WebApp 的 ClassLoader。需要加载 WebApp 下的类时,就取出 ClassLoader 来使用。

    • 方法内不能使用 static 修饰变量

    线程

    线程的五大状态:

    1. 新生(Born)
    2. 就绪(Runnable)
    3. 运行(Running)
    4. 消亡(Dead)
    5. 阻塞(Blocking)

    创建线程的方式:

    1. extends Thread
    2. implements Runnable

    控制线程的几种常见方法:

    • setPriority(int)
      设置线程的优先级,可选范围 1- 10,默认为 5,越大优先级越高;
      没什么意义,因为只是概率而已
    • static sleep(long)
    • join()
      当前线程邀请另一个线程优先执行,比如主线程里写 xx.join(); 意思就是主线程让 xx 线程执行完成后再执行,否则一直处于阻塞状态。
    • static yield()
      当前线程放弃持有的时间片,直接回到就绪,当然也有可能出现放弃时间片后又被 Cpu 选中的情况。
    • setName() + getName()
    • static activeCount()
      得到程序中所有活跃线程的总数,活跃线程:就绪 + 运行 + 阻塞;
      这个方法永远不可能返回 0,至少是 1.
    • static currentThread()
      得到当前线程对象,比如获得主线程的对象,在 run 方法调用的其他方法中使用;
      在 run 方法中没必要,直接 this 就是了。
    • setDaemon(true)
      设置成为守护进程,当程序中只剩下守护线程时会自动终结自己;
      Java 中著名的守护线程 GC,一般的特性:
      1. 通常是无限循环的
      2. 守护线程一般有极低的优先级
      3. 设置守护线程必须在 start 之前
    • interrupt()
      中断线程的阻塞状态,比如 sleep 时间还没到可以用 interrupt() 强制唤醒,但是会抛出一个异常。

    线程中所有静态方法不关注谁调用的,而是关注出现在哪里,出现在哪里就是操作那个线程。

    线程中所有涉及主动进入阻塞状态的方法都需要进行异常处理

    关于锁

    锁的出现就是为了解决并发错误,当多个线程共享同一个对象的时候,某一个线程未处理完成时 CPU 时间片就用尽了,然后就会出现并发错误。

    然后就需要加锁来保证不会出现错误,通常有两种方案:

    • 使用 synchronized 关键字
      叫做互斥锁,或者互斥锁标记,它可以修饰方法或者代码块,用在代码块上要显式的声明锁,用在方法上默认是 this。
      还有就是 synchronized 特性本身不会被继承
    • java.util.concurrent.locks.ReentrantLock
      可翻译为可重用锁,JDK1.5 加入的,遵循了 OO 思想,有两个方法:lock() 和 unlock()

    锁如果使用不当就会形成死锁,要解决死锁一般需要用到 Object 的三个方法:

    • wait()
      让当前线程放弃已持有的锁标记,并且进入调用方对象的等待池。
    • notify()
      唤醒调用方对象中等待池中的某个线程,是随机的。
    • notifyAll()
      唤醒调用方对象中等待池中的全部线程

    这三个方法都必须在已经持有锁标记的前提下才能使用,所以它们都必须出现在synchronized(){当中}

    锁池和等待池

    利用每一个对象都有一个锁旗标,拥有这个旗标后才可以访问此对象的资源,当线程无法获取此对象的锁旗标时就会发生阻塞进入此对象的锁池,等待旗标的释放,当释放后所有的等待旗标的线程会被唤醒,进入就绪状态争夺旗标。

    使用 wait 会进入等待池,遇到 synchrnized 会进入锁池;

    进入等待池会释放当时持有的锁,而锁池不会;

    锁池中,只要锁标记再度可用 线程自动离开,等待池 必须要 notify() 或者 notifyAll();

    关于离开的去向:离开锁池前往就绪;离开等待池前往锁池(之前释放了锁,必须得重新获取锁,既然有人唤醒它,说明此时旗标肯定在别人手里)

    关于线程池

    关于这一块,之前在 这里 已经写过了,这次提提怎么使用就够了:

    常规使用,用 ExecutorService 这个接口来写吧,以及线程的几种定义方式:

    public class TestThreadPool{
      public static void main(String[] args) throws Exception{
        ExecutorService es = Executors.newFixedThreadPool(2);
        //             newCachedThreadPool();
        //			   newSingleThreadExecutor()
        ThreadOne t1 = new ThreadOne();
        es.submit(t1);
        ThreadTwo t2 = new ThreadTwo();
        es.submit(t2);
        ThreadThree t3 = new ThreadThree();
        Future<String> f = es.submit(t3);
        System.out.println(f.get());
        //es.shutdown();
        es.shutdownNow();
      }
    }
    class ThreadThree implements Callable<String>{//JDK5.0
      @Override
      public String call() throws Exception{
        for(int i = 0;i<6666;i++){
          System.out.println("我是第三种方式");
        }
        return "End";
      }
    }
    
    class ThreadTwo implements Runnable{
      @Override
      public void run(){
        for(int i = 0;i<6666;i++){
          System.out.println("我是第二种方式");
        }
      }
    }
    
    class ThreadOne extends Thread{
      @Override
      public void run(){
        for(int i = 0;i<6666;i++){
          System.out.println("我是第一种方式");
        }
      }
    }
    

    线程池的创建官方推荐使用 Executors 来创建,常见的有 newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor;

    第一种就是最简单的,也是最常用的,会事先维护几个线程,等任务来直接执行;

    第二种是当有任务的时候再创建线程(避免浪费),任务执行完后会等待一分钟(默认),如果一分钟内没有新任务,那么此线程就会被终结;

    第三种是同一时间只允许一个线程执行,其他的任务都排队等着,适合用在秒杀的情况。

    shutdown 和 shutdownNow 的区别:

    shutdown :不再接受新的任务,当线程池中的任务(包括等待中的和正在执行的)执行完毕后销毁线程池。

    shutdownNow:试图停止所有正在执行的活动任务(一般情况正在执行的任务都会正常跑完的),暂停处理正在等待的任务,不再接受新任务,并返回等待执行的任务列表。

    还有就是通过实现 Callable<> 的 call 方法来定义线程,这种方式定义的线程解决了其他方式无法实现的问题:

    • run() 被定义为 void 无法返回数据
    • run() 没有任何 throws 声明

    这种方式定义的线程,只能通过线程池的方式来启动,并且 submit 的时候会返回一个 Future 对象,利用这个对象可以获得线程的返回值,就是调用其 get 方法,注意:当线程未执行完时,此方法一直是阻塞状态;当线程被意外终止那么 get 方法可能会一直卡在阻塞中(当然有重载可以指定等待的最大时间)。

    IO

    这就是通常我们所说的 IO 流了,流可分为三类:

    1. 方向分: 输入流 or 输出流
    2. 单位分: 字节流 or 字符流
    3. 功能分: 节点流 or 过滤流(包装流、处理流)

    File对象

    创建 File 对象的三种常见形式:

    new File(String 完整路径);
    new File(String 父目录,String 文件名);
    new File(File 父目录对象,String 文件名);
    

    然后介绍常用的几个方法:

    • static listRoots() : 得到当前计算机所有根目录

    • String[] list() : 动态的列出一个目录当中所有的文件名字

    • File[] listFiles() : 动态的列出一个目录当中所有的文件对象

    • exists() : 判断 File 对象指代的文件或者目录是否存在

    • isFile() : 判断 File 对象指代的是不是一个文件

    • isDirectory() : 判断 File 对象指代的是不是一个目录

    • length() : 得到文件的字节个数;只能对文件调用,对目录调用得到的没有意义

    • mkdirs() : 创建多层不存在的目录结构

    • getName() : 得到文件或者目录的名字

    • getParent() : 得到文件或者目录的父目录

    • getAbsolutePath() : 得到文件或者目录的绝对路径

    • setLastModified() : 设置文件的最后一次修改时间,设置的是时间戳

    • lastModified() : 得到文件的最后一次修改时间

    • delete() : 删除目录或者文件
      如果要删除的是一个目录 则必须保证目录是空的

    • renameTo() : 重命名文件或者目录
      例如:a.renameTo(c); a 代表源文件,必须存在;c 代表目标文件,必须不存在;
      其中 a 和 c 可以是不同的目录结构,从而实现剪切。

    过滤器

    使用 File 的 listFiles() 方法的时候可以传入一个文件过滤器(FileFilter),用来过滤指定的文件,这样能减轻接下来遍历的压力。

    FileFilter 是个接口,并且它只定义了一个方法:boolean accept(File pathname) 比如:

    class JavaFilter implements FileFilter{
      private JavaFilter(){}
      private static JavaFilter jf = new JavaFilter();
      public static JavaFilter getFilter(){
        return jf;
      }
    
      @Override
      public boolean accept(File f){
        return f.isFile() && f.getName().toLowerCase().endsWith(".java");
      }
    }
    
    class DirFilter implements FileFilter{
      private DirFilter(){}
      private static DirFilter df = new DirFilter();
      public static DirFilter getFilter(){
        return df;
      }
    
      @Override
      public boolean accept(File f){
        return f.isDirectory();
      }
    }
    
    // 使用 lambda
    File[] files = file.listFiles(f -> f.isFile() && f.getName().endsWith(".java"));
    File[] dirs = file.listFiles(f -> f.isDirectory());
    if(files == null) return;
    

    一个来过滤 Java 文件,一个来过滤目录,这里使用单例模式就比较适合了,另外,还可以直接使用 lambda 表达式,更加的爽

    字节流

    首先要认识的两个对象是:InputStream 和 OutputStream,他们分别是:所有字节输入流统一的父(抽象)类、所有字节输出流统一的父(抽象)类。

    方法一览:

    // 一次读一个字节,并返回这个字节
    int read();
    // 一次读一个数组,返回读取的长度
    int read(byte[] data);
    // 一次读一个数组,从 off 开始填充,填充 len 个
    int read(byte[] data,int off,int len);
    
    write(int data);
    write(byte[] data);
    write(byte[] data,int off,int len);
    

    需要注意:一次读一个字节但是返回的是 int,这是为了确保返回值 -1 表示文件的结束,假设读到了一个字节是 -1,那么先会进行类型提升到 int,这里的提升是 &0xff 来确保前面补的是 0 而不是 1。

    读取数组时,如果读到最后不足一个数组的大小,那么返回的 int 就不是 data.length,所以说并不是绝对的。


    上面说的是顶级的抽象类,下面就来说说最常用的两个具体类:FileInputStream 和 FileOutputStream。

    • 它们作为节点流,构造方法可以指定连接 String 文件名 File 对象

    • 虽然贵为节点流,但是它们只能连接文件 不能连接目录

    • 节点输出流连接的文件即便不存在在,创建流的时候也会被自动创建出来,但是如果连接的目录结构都不存在 则直接异常【File 类还有个 mkdirs()】

    • 节点输出流连接的文件即便已经存在,在创建流的一刻 也会被新的空白文件直接替换。
      如果我们的需求是想要在最后追加内容,那么构造方法:new FileOutputStream("abc.txt",true);

    • FileInputStream 最常用的是 read(byte[] data);;FileOutputStream 最常用的却是 write(byte[],int,int)

    • FileInputStream 以 -1 作为读取结束的标识

    下面看经典的复制文件的例子:

    public class FileCopy{
      public static void main(String[] args){
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try{
          fis = new FileInputStream("1.mp3");
          fos = new FileOutputStream("2.mp3");
          // 2^10 = 1024 也就是字节 <<10 是 kb,再 <<10 是 mb
          byte[] data = new byte[5<<20];  // 5MB
          int len = 0;
          while((len = fis.read(data)) != -1){
            fos.write(data,0,len);
          }
        }catch(Exception e){
          e.printStackTrace();
        }finally{
          try{
            fis.close();
          }catch(Exception e){
            e.printStackTrace();
          }finally{
            try{
              fos.close();
            }catch(Exception e){
              e.printStackTrace();
            }
          }
        }
      }
    }
    
    // 简写
    public class FileCopy2{
    	public static void main(String[] args){
    		// JDK7+ 特性。
    		try(FileInputStream fis = new FileInputStream("1.mp3");
    			FileOutputStream fos = new FileOutputStream("3.mp3")){
    			byte[] data = new byte[5<<20];
    			int len;
    			while((len = fis.read(data)) != -1){
    				fos.write(data,0,len);
    			}
    		}catch(Exception e){e.printStackTrace();}
    	}
    }
    

    设置缓冲大小使用位运算效率更高哦,只需要记住 x<<20 就是 xMB 大小的缓冲区。

    JDK7 后有了特性,就不用在 finally 里写这么恶心的代码了。。。。

    过滤流

    之所以称它们为过滤流是因为他们接收的对象是 stream,并不能直接传 file 或者路径,对于字节流来说,有几个还算用的多的字节流过滤流:

    BufferedInputStream、BufferedOutputStream、DataInputStream、DataOutputStream、ObjectInputStream、ObjectOutputStream。

    经过过滤(包装)后,后面的操作只需要对这个过滤流操作就行了,因为是装饰(包装)模式,最后关流的时候只关过滤流就 OK 了。


    前两个:

    作为过滤流的它们是为了给原本的节点流添加缓冲空间,从而提高每次读写的吞吐量 进而提高效率。它们构造方法的第二个参数可以指定缓冲空间大小(默认只有8192字节,也就是 8k);一定记得及时清空缓冲空间 防止数据滞留缓冲区,其中有三种情况会刷新缓冲区:1、当缓存空间满了的时候自动刷新;2、当关闭流操作时会自动刷新;3、手动调用 flush。

    这样,即使你调用 read 方法一个字节一个字节的读其实它会一次读指定的大小到缓冲区,然后一个个的给你;写也是,并不是一个个的写,而是写到缓冲区,满了以后一次性 flush 到硬盘。


    中间两个:

    是为了给原本的节点流添加读写基本数据类型的功能;DataInputStream 提供了一组方法 readXxxx();,DataOutputStream 提供一组方法 writeXxxx(); ,这时候不再以 -1 作为读取结束的标识了,而是如果已经到达文件结尾还继续读取,则直接出现 EOFException(End of File)。

    public static void main(String[] args){
      try(DataOutputStream dos = new DataOutputStream(new FileOutputStream("t.data"))){
        dos.writeInt(4545);
      }catch(Exception e){e.printStackTrace();}
    
      try(DataInputStream dis = new DataInputStream(new FileInputStream("t.data"))){
        int x = dis.readInt();
        System.out.println(x);
      }catch(Exception e){e.printStackTrace();}
    }
    

    最后两个:

    给原本的节点流添加读写对象的功能的,与上面类似,对应的方法就是 readObject();writeObject();

    同样不以 -1 作为结束,而也会 EOFException。

    要写出的对象必须先要序列化 (implements Serializable),如果要持久化的对象当中有其它引用类型的属性,那么也要进行序列化标识;但是如果某些属性无关紧要,不需要保存(那就相当于是 null 了),可以直接使用 transient 修饰。

    如果要持久化的是一个使用了比较器的 TreeSet 或者 TreeMap,就连比较器的类型也要实现序列化接口。

    字符流

    和字节流一样,两大鼻祖:Read 和 Writer;接口中定义的方法也和字节流中的那三个对应,就不写了。

    两个常用的具体类:FileReader 和 FileWriter,描述也不多说了,和上面 FileInputStream 和 FileOutputStream 一样一样的

    过滤流

    对于字符流来说,最常用的还是莫过于这里说的过滤流,因为对于字符操作,过滤流提供了更方便的方法。

    BufferedReader 和 BufferedWriter:

    通过对原有的字符流添加缓冲空间,使其可以支持一次读取一行(readLine),和一次写入一个字符串(write + newLine 换行)。其中 BufferReader 使用的非常频繁,必须要熟练的。

    PrintStream 和 PrintWriter:

    像是一对姐妹,他们的方法一致,不同的是一个输出字节,一个输出字符;PrintStream 我们最长见的就是打印语句中的 out,它就是 PrintStream 类型的,然后 PrintWriter 在 IO 操作中用的非常频繁,相比 BufferedWriter 它更加的好用。

    既然说好用,那就来看看它的特点吧:

    • 既可以作为节点流,又可以作为过滤流;也就是可以直接往构造函数里扔文件对象、路径、节电流

    • 既可以连接字节流,又可以连接字符流;是的,构造函数里都可以扔,不需要转换流

    • 当做节点流的时候,构造方法第二个参数可以指定字符集

    • 当做过滤流的时候,构造方法第二个参数可以指定自动清空缓冲
      例如:new PrintWriter(new FileWriter("a.txt",true),true);
      第一个 true 是开启追加模式,第二个是自动刷新(flush)

    • 拥有 println() 方法,等价于 write() + newLine()


    转换流(桥转换器):InputStreamReader 和 OutputStreamReader ,其中最常用的是 InputStreamReader,实际用法例如:

    new BufferedReader(new InputStreamReader(new FileInputStream("a.txt")))

    而 OutputStreamReader 基本不怎么用,因为有 PrintWriter 啊,它可以字符流字节流通吃,也就是说内部会内置一个转换流,就是这个 OutputStreamReader ,所以让我们方便了。


    再来补充个 RandomAccessFile 用来支持随机文件的读取和写入,通常可以用它来占空间,然后用流来对其进行写:

    RandomAccessFile raf = new RandomAccessFile("d:\abc.mp4","rw");
    File d = new File("d:\");
    long free = d.getFreeSpace();//得到d盘的剩余空间
    raf.setLength(free);
    raf.close();
    

    这只是个简单的使用,这里先 TODO

    原码反码补码

    对于一个数, 计算机要使用一定的编码方式进行存储. 原码, 反码, 补码是机器存储一个具体数字的编码方式.

    原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值,原码是人脑最容易理解和计算的表示方式。

    [+1]原 = 0000 0001
    
    [-1]原 = 1000 0001
    

    正数的反码是其本身;负数的反码是在其原码的基础上, 符号位不变,其余各个位取反.

    [+1] = [00000001]原 = [00000001]反
    
    [-1] = [10000001]原 = [11111110]反
    

    正数的补码就是其本身;负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)

    [+1] = [00000001]原 = [00000001]反 = [00000001]补
    
    [-1] = [10000001]原 = [11111110]反 = [11111111]补
    

    那么为何要使用原码, 反码和补码?

    首先, 因为人脑可以知道第一位是符号位, 在计算的时候我们会根据符号位, 选择对真值区域(可理解为不加符号位的二进制表示)的加减。

    但是对于计算机,加减乘数已经是最基础的运算,要设计的尽量简单; 计算机辨别 "符号位" 显然会让计算机的基础电路设计变得十分复杂!

    于是人们想出了将符号位也参与运算的方法; 我们知道, 根据运算法则减去一个正数等于加上一个负数, 即: 1-1 = 1 + (-1) = 0 , 所以机器可以只有加法而没有减法,这样计算机运算的设计就更简单了.

    于是人们开始探索 将符号位参与运算, 并且只保留加法的方法, 首先来看原码:

    计算十进制的表达式: 1-1=0

    1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原 = -2

    如果用原码表示, 让符号位也参与计算, 显然对于减法来说, 结果是不正确的;这也就是为何计算机内部不使用原码表示一个数.

    为了解决原码做减法的问题, 出现了反码:

    计算十进制的表达式: 1-1=0

    1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原= [0000 0001]反 + [1111 1110]反 = [1111 1111]反 = [1000 0000]原 = -0

    发现用反码计算减法, 结果的真值部分是正确的.

    而唯一的问题其实就出现在 "0" 这个特殊的数值上, 虽然人们理解上 +0 和 -0 是一样的,但是 0 带符号是没有任何意义的, 而且会有 [0000 0000]原 和 [1000 0000]原 两个编码表示 0.

    于是补码的出现, 解决了 0 的符号以及两个编码的问题:

    1-1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]补 + [1111 1111]补 = [0000 0000]补=[0000 0000]原

    这样 0 用 [0000 0000] 表示, 而以前出现问题的 -0 则不存在了;而且可以用 [1000 0000] 表示 -128:

    (-1) + (-127) = [1000 0001]原 + [1111 1111]原 = [1111 1111]补 + [1000 0001]补 = [1000 0000]补

    -1-127 的结果应该是 -128, 在用补码运算的结果中 [1000 0000]补 就是 -128, 但是注意因为实际上是使用以前的 -0 的补码来表示 -128, 所以 -128 并没有原码和反码表示(所以可以多表示一个最低数).(对 -128 的补码表示 [1000 0000]补 算出来的原码是 [0000 0000]原, 这是不正确的)

    使用补码, 不仅仅修复了 0 的符号以及存在两个编码的问题, 而且还能够多表示一个最低数; 这就是为什么 8 位二进制, 使用原码或反码表示的范围为 [-127, +127], 而使用补码表示的范围为 [-128, 127].

    因为机器使用补码, 所以对于编程中常用到的 32 位 int 类型,可以表示范围是: [-231, 231-1] 因为第一位表示的是符号位.而使用补码表示时又可以多保存一个最小值.

    出自:https://www.cnblogs.com/zhangziqiu/archive/2011/03/30/ComputerCode.html

    其他

    查找一个字符串出现多少次?

    可以使用 (str + "l").split("abc").length - 1 ,源字符串加任意一个字符防止被 split 的字符在最后会导致少一个,然后它的 length - 1 就是个数。

  • 相关阅读:
    SQL 语法总结
    终于开始用github了
    前端开发第一阶段总结
    windows系统快捷操作の高级篇
    windows系统快捷操作の进阶篇
    windows系统快捷操作の基础篇
    安装使用ubuntu问题汇总
    十进制转任意进制
    任意进制转10进制
    爬取妹子图(requests + BeautifulSoup)
  • 原文地址:https://www.cnblogs.com/bfchengnuo/p/9193007.html
Copyright © 2020-2023  润新知