【学习】java基础整理(二)

传送门

java基础整理(一)

51. 类 ExampleA 继承 Exception,类 ExampleB 继承 ExampleA 请问执行此段代码的输出是什么?

try {
	throw new ExampleB("b")
} catch(ExampleA e){
	System.out.println("ExampleA");
} catch(Exception e){
	System.out.println("Exception");
}

输出:ExampleA。

根据里氏代换原则"能使用父类型的地方一定能使用子类型"因此抛出的ExampleB("b")能够被抓取其父类ExampleA类型异常的 catch 捕获,同时由于catch仅触发一次,所以不考虑后面的catch(即使将第二个catch挪到前面,也不能捕获)。

52. List、Set、Map 是否继承自 Collection 接口?

List、Set 是,Map 不是

Map键值对映射容器独立接口,与 List 和 Set 有明显的区别,而 Set 存储的零散的元素且不允许有重复元素(数学中的集合也是如此),List是线性结构的容器,适用于按数值索引访问元素的情形。

53. 阐述 ArrayList、Vector、LinkedList 的存储性能和特性。

ArrayListVector 都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector 中的方法由于添加了 synchronized 修饰,因此 Vector线程安全的容器,但性能上较ArrayList 差因此已经是 Java 中的遗留容器

LinkedList 使用双向链表实现存储(将内存中零散的内存单元通过附加的引用关联起来,形成一个可以按序号索引的线性结构,这种链式存储方式与数组的连续存储方式相比,内存的利用率更高),按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快

Vector 属于遗留容器(Java 早期的版本中提供的容器,除此之外,Hashtable、Dictionary、BitSet、Stack、Properties都是遗留容器),已经不推荐使用,但是由于 ArrayList 和 LinkedListed 都是非线程安全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类Collections 中的 synchronizedList 方法将其转换成线程安全的容器后再使用(这是对装潢模式的应用,将已有对象传入另一个类的构造器中创建新的对象来增强实现)。

54. Collection 和 Collections 的区别?

  • Collection 是一个接口,它是 Set、List 等容器的父接口
  • Collections 是一个工具类,提供了一系列的静态方法来辅助容器操作,这些方法包括对容器的搜索、排序、线程安全化等等。

55. List、Map、Set 三个接口存取元素时,各有什么特点?

List 以特定索引来存取元素,可以有重复元素Set 不能存放重复元素(用对象的equals()方法来区分元素是否重复)。Map 保存键值对(key-value pair)映射,映射关系可以是一对一或多对一。SetMap 容器都有基于哈希存储排序树两种实现版本,基于哈希存储的版本理论存取时间复杂度为 O(1),而基于排序树版本的实现在插入或删除元素时会按照元素或元素的键(key)构成排序树从而达到排序和去重的效果。

56. TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素?

TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进行排序。

// 1. TreeSet
// 类实现Comparable,重写compareTo方法
public class Student implements Comparable{ 
    private String name; 
    private int age;
	@Override
    public int compareTo(Student o) {
        //按照对象的年龄进行排序
        //主要判断条件: 按照年龄从小到大排序
        int result = this.age - o.age;
        //次要判断条件: 年龄相同时,按照姓名的字母顺序排序
        result = result == 0 ? this.name.compareTo(o.getName()) : result;
        return result;
    }
}
TreeSet<Student> ts = new TreeSet<>(); 
//创建学生对象 
Student s1 = new Student("zhangsan",28); 
Student s2 = new Student("lisi",27); 
Student s3 = new Student("wangwu",29); 
ts.add(s1); 
ts.add(s2); 
ts.add(s3);
// 此时ts中的学生对象就是按照重写的compareTo规则进行排序
// =============================================================

// 2. TreeMap
TreeMap<String, Integer> treeMap = new TreeMap<>(new Comparator<String>() {
    @Override
    public int compare(String o1, String o2) {
        return o2.compareTo(o1); 		//按字符串降序
    }
});
treeMap.put("feifei", 28);
treeMap.put("zhang", 36);
treeMap.put("yong", 35);
treeMap.put("xin", 34);

Collections 工具类的 sort 方法有两种重载的形式:

  1. 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;
  2. 第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是 Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。

57. Thread 类的 sleep()方法和对象的 wait()方法都可以让线程暂停执行,它们有什么区别?

sleep()方法(休眠)是线程类(Thread)静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态,参考 第66点的线程状态转换图)。

wait()是 Object 类的方法,调用对象的 wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的 notify()方法(或 notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。

补充:

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位线程进程一个实体,是 CPU 调度和分派的基本单位,是比进程更小的能独立运行的基本单位线程的划分尺度小于进程,这使得多线程程序的并发性高进程在执行时通常拥有独立的内存单元,而线程之间可以共享内存。使用多线程的编程通常能够带来更好的性能和用户体验,但是多线程的程序对于其他程序是不友好的,因为它可能占用了更多的 CPU 资源。当然,也不是线程越多,程序的性能就越好,因为线程之间的调度和切换也会浪费 CPU 时间。时下很时髦的 Node.js就采用了单线程异步 I/O 的工作模式

58. 线程的 sleep()方法和 yield()方法有什么区别?

  1. sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
  2. 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;
  3. sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
  4. sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性

59. 当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的 synchronized 方法 B?

不能。

其它线程只能访问该对象的非同步方法同步方法则不能进入。因为非静态方法上的 synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入A 方法说明对象锁已经被取走,那么试图进入 B 方法的线程就只能在等锁池中等待对象的锁(不是等待池)。

60. 请说出与线程同步以及线程调度相关的方法。

  • wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的

  • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常

  • notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关

  • notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

61. 编写多线程程序有几种实现方式?

Java 5 以前实现多线程有两种实现方法:一种是继承 Thread 类;另一种是实现Runnable 接口。两种方式都要通过重写 run()方法来定义线程的行为,推荐使用后者,因为 Java 中的继承是单继承,一个类有一个父类,如果继承了 Thread 类就无法再继承其他类了,显然使用 Runnable 接口更为灵活。

补充:Java 5 以后创建线程还有第三种方式:实现 Callable 接口,该接口中的 call方法可以在线程执行结束时产生一个返回值,代码如下所示:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
class MyTask implements Callable<Integer> {
    private int upperBounds;
    public MyTask(int upperBounds) {
        this.upperBounds = upperBounds;
    }
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for(int i = 1; i <= upperBounds; i++) {
	        sum += i;
        }
        return sum;
    }
}
class Test {
    public static void main(String[] args) throws Exception {
        List<Future<Integer>> list = new ArrayList<>();
        ExecutorService service = Executors.newFixedThreadPool(10);
        for(int i = 0; i < 10; i++) {
            list.add(service.submit(new MyTask((int) (Math.random() *100))));
        }
        int sum = 0;
        for(Future<Integer> future : list) {
            // while(!future.isDone()) ;
            sum += future.get();
        }
        System.out.println(sum);
    }
}

62. synchronized 关键字的用法?

synchronized 关键字可以将对象或者方法标记为同步,以实现对对象和方法的互斥访问,如下所示:

// 1. 在方法前加
public synchronized void deposit(double money) {
    double newBalance = balance + money;
    try {
	    Thread.sleep(10); // 模拟此业务需要一段处理时间
    }
    catch(InterruptedException ex) {
    	ex.printStackTrace();
    }
    balance = newBalance;
}

// 调用方法时加
@Override
public void run() {
    synchronized (account) {
        account.deposit(money);
    }
}

63. 举例说明同步和异步。

如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就必须进行同步存取(数据库操作中的排他锁就是最好的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作。

64. 启动一个线程是调用 run()还是 start()方法?

启动一个线程是调用 start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由 JVM 调度并执行,这并不意味着线程就会立即运行。run()方法是线程启动后要进行回调(callback)的方法。

65. 什么是线程池(thread pool)?

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。

Java 5+中的 Executor 接口定义一个执行线程的工具。它的子类型即线程池接口是 ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类Executors 面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:

  • newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行;
  • newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  • newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
  • newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求;

如果在服务器上使用线程池,建议使用newFixedThreadPool方法创建线程池,这样能获得更好的性能。

66. 线程的基本状态以及状态之间的关系?

说明:其中 Running 表示运行状态,Runnable 表示就绪状态(万事俱备,只欠CPU),Blocked 表示阻塞状态,阻塞状态又有多种情况,可能是因为调用 wait()方法进入等待池,也可能是执行同步方法或同步代码块进入等锁池,或者是调用了 sleep()方法或 join()方法等待休眠或其他线程结束,或是因为发生了 I/O 中断。

67. 简述 synchronized 和 java.util.concurrent.locks.Lock的异同?

LockJava 5 以后引入的新的 API,和关键字 synchronized 相比

  • 主要相同点:Lock 能完成 synchronized 所实现的所有功能
  • 主要不同点:Lock 有比synchronized 更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁synchronized 会自动释放锁,而 Lock 一定要求程序员手工释放(unlock),并且最好在 finally 块中释放(这是释放外部资源的最好的地方)。

如62中的问题,用lock可用如下写法解决

private Lock accountLock = new ReentrantLock();
private double balance; // 账户余额
// 存款
public void deposit(double money) {
    // 存款之前先加锁
    accountLock.lock();
    try {
	    double newBalance = balance + money;
        try {
            Thread.sleep(10); // 模拟此业务需要一段处理时间
        }
        catch (InterruptedException ex) {
            ex.printStackTrace();
        }
	    balance = newBalance;
    }
    finally {
        // 任务结束解锁
	    accountLock.unlock();
    }
}

68. Java 中如何实现序列化,有什么意义?

序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决对象流读写操作时可能引发的问题(如果不进行序列化可能会存在数据乱序的问题)。要实现序列化,需要让一个类实现 Serializable 接口,该接口是一个标识性接口标注该类对象是可被序列化的,然后使用一个输出流来构造一个对象输出流并通过writeObject(Object)方法就可以将实现对象写出(即保存其状态);如果需要反序列化则可以用一个输入流建立对象输入流,然后通过 readObject 方法从流中读取对象。序列化除了能够实现对象的持久化之外还能够用于对象的深度克隆(可以参考第 29 题)

69. Java 中有几种类型的流?

字节流和字符流

  • 字符流:是块读写,带有缓存,处理的单元为2个字节的Unicode字符,分别操作字符字符数组字符串,属性范围小,只能字符这一种类型。
  • 字节流:是字节读写没有缓存,处理单元为1个字节,操作字节字节数组,属性范围大,可以是字符,可以是二进制文件,可以是音频,可以是各种各样的类型,只要符合字节形式存储的都可以接字节流。

字节流继承于 InputStreamOutputStream字符流继承于ReaderWriter。在 java.io 包中还有许多其他的流,主要是为了提高性能和使用方便。关于 Java 的 I/O 需要注意的有两点:一是两种对称性(输入和输出的对称性,字节和字符的对称性);二是两种设计模式(适配器模式和装潢模式)。另外 Java 中的流不同于 C#的是它只有一个维度一个方向。

补充:编程实现文件拷贝。

// 这里都使用了java7引入的新特性,TWR(Try-with-resources),能够保证try()声明的资源在try结束后关闭,不用再写finally去close了

// 方法一
public static void fileCopy(String source, String target) throws IOException {
    try (InputStream in = new FileInputStream(source)) {
        try (OutputStream out = new FileOutputStream(target)) {
            byte[] buffer = new byte[4096];
            int bytesToRead;
            while ((bytesToRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesToRead);
            }
        }
    }
}

// 方法二
public static void fileCopyNIO(String source, String target) throws IOException {
    try (FileInputStream in = new FileInputStream(source)) {
        try (FileOutputStream out = new FileOutputStream(target)) {
            FileChannel inChannel = in.getChannel();
            FileChannel outChannel = out.getChannel();
            ByteBuffer buffer = ByteBuffer.allocate(4096);
            while (inChannel.read(buffer) != -1) {
                buffer.flip();
                outChannel.write(buffer);
                buffer.clear();
            }
        }
    }
}

70. 写一个方法,输入一个文件名和一个字符串,统计这个字符串在这个文件中出现的次数。

public static int countWordInFile(String filename, String word) {
    int counter = 0;
    try (FileReader fr = new FileReader(filename)) {
        try (BufferedReader br = new BufferedReader(fr)) {
            String line = null;
            //br.readLine() 赋值给line后,对line是否等于null进行判断
            while ((line = br.readLine()) != null) {
                int index = -1;
                while (line.length() >= word.length() && 
                       (index =line.indexOf(word)) >= 0) {
                    counter++;
                    line = line.substring(index + word.length());
                }
            }
        }
    } catch (Exception ex) {
        ex.printStackTrace();
    }
    return counter;
}

71. 如何用 Java 代码列出一个目录下所有的文件?

// 方法一,仅列出目录下文件名
File f = new File("D:\\xxx");
for(File temp : Objects.requireNonNull(f.listFiles())) {
    if(temp.isFile()) {
        System.out.println(temp.getName());
    }
}

// 方法二,子文件也打印
class xxx {
    public static void main(String[] args) {
	    showDirectory(new File("D:\\xxx");
    }
    public static void showDirectory(File f) {
	    _walkDirectory(f, 0);
    }
    private static void _walkDirectory(File f, int level) {
        if(f.isDirectory()) {
            for(File temp : f.listFiles()) {
            _walkDirectory(temp, level + 1);
	        }
        }
        else {
            for(int i = 0; i < level - 1; i++) {
	            System.out.print("\t");
            }
            System.out.println(f.getName());
        }
    }
}
                      
// NIO是java 1.4引入的一套新的IO API,可以替代标准java IO api,支持面向缓冲区的、基于通道的IO操作。传统IO面向流
// NIO.2 是java1.7在原有NIO基础上新增了Files、Paths工具类
// 方法三,采用Java7 NIO.2特性
class ShowFileTest {
    public static void main(String[] args) throws IOException {
        Path initPath = Paths.get("D:\\xxx");
        Files.walkFileTree(initPath, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributesattrs)
            throws IOException {
	            System.out.println(file.getFileName().toString());
	            return FileVisitResult.CONTINUE;
            }
        });
    }
}

72. 用 Java 的套接字编程实现一个多线程的回显(echo)服务器。

所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。

echo服务器,描述起来很简单,服务端收到什么,就给客户端发送什么。

服务器端:


public class EchoServer {
    private static final int ECHO_SERVER_PORT = 6789;

    public static void main(String[] args) {
        // 指定端口创建一个基于TCP/IP协议的服务器
        try (ServerSocket server = new ServerSocket(ECHO_SERVER_PORT)) {
            System.out.println("服务器已经启动...");
            while (true) {
                // 从连接请求队列中取出一个客户的连接请求,然后创建与客户连接的Socket对象,并将它返回。
                // 如果队列中没有连接请求,accept()方法就会一直等待,直到接收到了连接请求才返回。
                Socket client = server.accept();
                // 连接成功后,传入Socket并打开一个线程
                new Thread(new ClientHandler(client)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

//    用一个静态内部类实现线程的功能,使用多线程可以避免一个用户 I/O 操作所产生的中断影响其他用户对服务器的访问,
//    简单的说就是一个用户的输入操作不会造成其他用户的阻塞。
//    当然使用线程池可以获得更好的性能,因为频繁的创建和销毁线程所造成的开销也是不可忽视的。
    private static class ClientHandler implements Runnable {
        private Socket client;

        public ClientHandler(Socket client) {
            this.client = client;
        }

        @Override
        public void run() {

            // client.getInputStream()得到字节输入流,通过InputStreamReader将字节流转为字符流
            // BufferedReader从字符输入流中读取文本并缓冲字符
            try (BufferedReader br = new BufferedReader(new
                    InputStreamReader(client.getInputStream()));
                 PrintWriter pw = new
                         PrintWriter(client.getOutputStream())) {
                String msg = br.readLine();
                // getInetAddress获取对象包含的ip地址
                System.out.println("收到" + client.getInetAddress() + " 发送的: " + msg);
                // 将收到的信息写入输出流,返回给客户端
                pw.println(msg);
                pw.flush();
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                try {
                    client.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

客户端:

public class EchoClient {
    public static void main(String[] args) throws Exception {
        // 创建连接,使用6789端口连接localhost
        Socket client = new Socket("localhost", 6789);
        // 输入传输的信息
        Scanner sc = new Scanner(System.in);
        System.out.print("请输入内容: ");
        String msg = sc.nextLine();
        sc.close();
        // client.getOutputStream()得到字节输出流,并转为打印流
        // 输出流指的是客户端发送给服务器端的数据
        PrintWriter pw = new PrintWriter(client.getOutputStream());
        // 将控制台输入数据写入打印流并刷新
        pw.println(msg);
        pw.flush();
        // client.getInputStream()得到字节输入流,通过InputStreamReader将字节流转为字符流
        // BufferedReader从字符输入流中读取文本并缓冲字符
        // 输入流流指的是服务器发送给客户端的数据
        BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));
        // 打印到控制台
        System.out.println(br.readLine());
        client.close();
    }
}

73. XML 文档定义有几种形式?它们之间有何本质区别?解析XML 文档有哪几种方式?

  1. XML 文档定义分为 DTDSchema 两种形式,二者都是对 XML 语法的约束,其本质区别在于 Schema 本身也是一个 XML 文件,可以被 XML 解析器解析,而且可以为 XML 承载的数据定义类型,约束能力较之 DTD 更强大

  2. 对 XML 的解析主要有 DOM(文档对象模型,Document Object Model)、SAX(Simple API forXML)和 StAX(Java 6 中引入的新的解析 XML 的方式,Streaming API for XML)

    • 其中 DOM 处理大型文件时其性能下降的非常厉害,这个问题是由 DOM 树结构占用的内存较多造成的,而且 DOM 解析方式必须在解析文件之前把整个文档装入内存,适合对 XML 的随机访问(典型的用空间换取时间的策略);
    • SAX 是事件驱动型的 XML 解析方式,它顺序读取 XML 文件,不需要一次全部装载整个文件。当遇到像文件开头,文档结束,或者标签开头与标签结束时,它会触发一个事件,用户通过事件回调代码来处理 XML 文件,适合对 XML 的顺序访问;
    • StAX 把重点放在上,实际上 StAX 与其他解析方式的本质区别就在于应用程序能够把 XML 作为一个事件流来处理。将 XML 作为一组事件来处理的想法并不新颖(SAX 就是这样做的),但不同之处在于 StAX 允许应用程序代码把这些事件逐个拉出来,而不用提供在解析器方便时从解析器中接收事件的处理程序。

74. 你在项目中哪些地方用到了 XML?

XML 的主要作用有两个方面:数据交换信息配置。在做数据交换时,XML 将数据用标签组装起来,然后压缩打包加密后通过网络传送给接收者,接收者解密与解压缩后再从 XML 文件中还原相关信息进行处理,XML 曾经是异构系统间交换数据的事实标准,但此项功能几乎已经被 JSON(JavaScript Object Notation)取而代之。当然,目前很多软件仍然使用 XML 来存储配置信息,我们在很多项目中通常也会将作为配置信息的硬代码写在 XML 文件中,Java 的很多框架也是这么做的,而且这些框架都选择了 dom4j 作为处理 XML 的工具,因为 Sun 公司的官方API 实在不怎么好用。

补充:现在有很多时髦的软件(如 Sublime)已经开始将配置文件书写成 JSON格式,我们已经强烈的感受到 XML 的另一项功能也将逐渐被业界抛弃。

75. 阐述 JDBC 操作数据库的步骤。

  1. 加载驱动
  2. 创建连接
  3. 创建语句
  4. 执行语句
  5. 获取结果
  6. 关闭资源

补充:关闭外部资源的顺序应该和打开的顺序相反,也就是说先关闭 ResultSet(结果集)、再关闭 Statement(已编译语句)、再关闭 Connection(数据库连接)。虽然通常情况下在关闭连接时,连接上创建的语句和打开的游标也会关闭,但不能保证总是如此,因此应该按照刚才说的顺序分别关闭。此外,第一步加载驱动在 JDBC 4.0 中是可以省略的(自动从类路径中加载驱动),但是建议保留。

76. Statement 和 PreparedStatement 有什么区别?哪个性能更好?

PreparedStatement更好。

  1. PreparedStatement 接口代表预编译的语句,它主要的优势在于可以减少 SQL 的编译错误并增加 SQL 的安全性减少 SQL 注射攻击的可能性);
  2. PreparedStatement 中的 SQL 语句是可以带参数的,避免了用字符串连接拼接 SQL 语句的麻烦和不安全
  3. 当批量处理 SQL 或频繁执行相同的查询时,PreparedStatement 有明显的性能上的优势,由于数据库可以将编译优化后的SQL 语句缓存起来,下次执行相同结构的语句时就会很快(不用再次编译和生成执行计划)。

补充:为了提供对存储过程的调用,JDBC API 中还提供了 CallableStatement 接口。存储过程(Stored Procedure)是数据库中一组为了完成特定功能的 SQL 语句的集合,经编译后存储在数据库中,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。虽然调用存储过程会在网络开销、安全性、性能上获得很多好处,但是存在如果底层数据库发生迁移时就会有很多麻烦,因为每种数据库的存储过程在书写上存在不少的差别。

77. 使用 JDBC 操作数据库时,如何提升读取数据的性能?如何提升更新数据的性能?

  • 要提升读取数据的性能,可以通过结果集(ResultSet)对象setFetchSize()方法指定每次抓取的记录数(典型的空间换时间策略);
  • 要提升更新数据的性能,可以使用 PreparedStatement 语句构建批处理,将若干 SQL 语句置于一个批处理中执行。

78. 在进行数据库编程时,连接池有什么作用?

由于创建连接和释放连接都有很大的开销(尤其是数据库服务器不在本地时,每次建立连接都需要进行 TCP 的三次握手,释放连接需要进行 TCP 四次握手,造成的开销是不可忽视的),为了提升系统访问数据库的性能,可以事先创建若干连接置于连接池中,需要时直接从连接池获取,使用结束时归还连接池而不必关闭连接,从而避免频繁创建和释放连接所造成的开销,这是典型的用空间换取时间的策略(浪费了空间存储连接,但节省了创建和释放连接的时间)。

池化技术在Java 开发中是很常见的,在使用线程时创建线程池的道理与此相同。基于 Java 的开源数据库连接池主要有:C3P0、Proxool、DBCP、BoneCP、Druid 等。

补充:在计算机系统中时间和空间是不可调和的矛盾,理解这一点对设计满足性能要求的算法是至关重要的。大型网站性能优化的一个关键就是使用缓存,而缓存跟上面讲的连接池道理非常类似,也是使用空间换时间的策略。可以将热点数据置于缓存中,当用户查询这些数据时可以直接从缓存中得到,这无论如何也快过去数据库中查询。当然,缓存的置换策略等也会对系统性能产生重要影响,对于这个问题的讨论已经超出了这里要阐述的范围。

79. 什么是 DAO 模式?

DAO(Data Access Object)顾名思义是一个为数据库或其他持久化机制提供了抽象接口对象,在不暴露底层持久化方案实现细节的前提下提供了各种数据访问操作。在实际的开发中,应该将所有对数据源的访问操作进行抽象化后封装在一个公共 API 中。用程序设计语言来说,就是建立一个接口,接口中定义了此应用程序中将会用到的所有事务方法。在这个应用程序中,当需要和数据源进行交互的时候则使用这个接口,并且编写一个单独的类来实现这个接口,在逻辑上该类对应一个特定的数据存储。DAO 模式实际上包含了两个模式,一是 Data Accessor(数据访问器),二是 Data Object(数据对象),前者要解决如何访问数据的问题,而后者要解决的是如何用对象封装数据。

80. 事务的 ACID 是指什么?

事务就是用户定义的一系列数据库操作,这些操作可以视为一个完成的逻辑处理工作单元,要么全部执行,要么全部不执行,是不可分割的工作单元。

  • 原子性(Atomic):事务中各项操作,要么全做要么全不做,任何一项操作的失败都会导致整个事务的失败;

  • 一致性(Consistent):事务结束后系统状态是一致的;

  • 隔离性(Isolated):并发执行的事务彼此无法看到对方的中间状态;

  • 持久性(Durable):事务完成后所做的改动都会被持久化,即使发生灾难性的失败。通过日志和同步备份可以在故障发生后重建数据。

补充:关于事务,在面试中被问到的概率是很高的,可以问的问题也是很多的。首先需要知道的是,只有存在并发数据访问时才需要事务。当多个事务访问同一数据时,可能会存在 5 类问题,包括 3 类数据读取问题(脏读、不可重复读和幻读)和 2 类数据更新问题(第 1 类丢失更新和第 2 类丢失更新)。

  • 脏读(Dirty Read):A 事务读取 B 事务尚未提交的数据并在此基础上操作,而 B事务执行回滚,那么 A 读取到的数据就是脏数据;
  • 不可重复读(Unrepeatable Read):事务 A 重新读取前面读取过的数据,发现该数据已经被另一个已提交的事务 B 修改过了;
  • 幻读(Phantom Read):事务 A 重新执行一个查询,返回一系列符合查询条件的行,发现其中插入了被事务 B 提交的行;
  • 第 1 类丢失更新:事务 A 撤销时,把已经提交的事务 B 的更新数据覆盖了;
  • 第 2 类丢失更新:事务 A 覆盖事务 B 已经提交的数据,造成事务 B 所做的操作丢失。

数据并发访问所产生的问题,在有些场景下可能是允许的,但是有些场景下可能就是致命的,数据库通常会通过锁机制来解决数据并发访问问题,按锁定对象不同可以分为表级锁行级锁;按并发事务锁定关系可以分为共享锁独占锁

81. JDBC 中如何进行事务处理?

事务是访问数据库的一个操作序列,数据库应用系统通过事务集来完成对数据库的存取。事务的正确执行使得数据库从一种状态转换成另一种状态。

Connection 提供了事务处理的方法,通过调用 setAutoCommit(false)可以设置手动提交事务;当事务完成后用 commit()显式提交事务;如果在事务处理过程中发生异常则通过 rollback() 进行事务回滚。除此之外,从 JDBC 3.0 中还引入了 Savepoint(保存点)的概念,允许通过代码设置保存点并让事务回滚到指定的保存点。

82. JDBC 能否处理 Blob 和 Clob?

Blob 是指二进制大对象(Binary Large Object),而 Clob 是指大字符对象(Character Large Objec),因此其中 Blob 是为存储大的二进制数据而设计的,而 Clob 是为存储大的文本数据而设计的

JDBC 的 PreparedStatementResultSet 都提供了相应的方法来支持 BlobClob 操作。

83. 简述正则表达式及其用途。

在编写处理字符串的程序时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。

说明:计算机诞生初期处理的信息几乎都是数值,但是时过境迁,今天我们使用计算机处理的信息更多的时候不是数值而是字符串,正则表达式就是在进行字符串匹配和处理的时候最为强大的工具,绝大多数语言都提供了对正则表达式的支持。

84. Java 中是如何支持正则表达式操作的?

Java 中的 String 类提供了支持正则表达式操作的方法,包括:matches()、replaceAll()、replaceFirst()、split()。此外,Java 中可以用 Pattern 类表示正则表达式对象,它提供了丰富的 API 进行各种正则表达式操作,请参考下面面试题的代码。

补充: 如果要从字符串中截取第一个英文左括号之前的字符串,例如:北京市(朝阳区)(西城区)(海淀区),截取结果为:北京市,那么正则表达式怎么写?

String str = "北京市(朝阳区)(西城区)(海淀区)";
Pattern p = Pattern.compile(".*?(?=\\()");
Matcher m = p.matcher(str);
if(m.find()) {
	System.out.println(m.group());
}

85. 获得一个类的类对象有哪些方式?

  1. 类型.class,例如:String.class
  2. 对象.getClass(),例如:”hello”.getClass()
  3. Class.forName(),例如:Class.forName(“java.lang.String”)

86. 如何通过反射创建对象?

反射:是java中一种强大的工具,能够创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码链接。但是反射使用不当会成本很高。

反射机制的作用:

  1. 反编译:.class-->.java
  2. 通过反射机制访问java对象的属性,方法,构造方法等;

实现方法

  1. 通过类对象调用 newInstance()方法,例如:String.class.newInstance()
  2. 通过类对象的 getConstructor()getDeclaredConstructor()方法获得构造器(Constructor)对象并调用其 newInstance()方法创建对象,例如:String.class.getConstructor(String.class).newInstance(“Hello”);

87. 如何通过反射获取和设置对象私有字段的值?

可以通过类对象的 getDeclaredField()方法获取字段(Field)对象,然后再通过字段对象的 setAccessible(true)将其设置为可以访问,接下来就可以通过 get/set 方法来获取/设置字段的值了。

88. 如何通过反射调用对象的方法?

// 定义String对象
String str = "hello";
// 用getClass获取String类,再通过getMethod获取toUpperCase方法
Method m = str.getClass().getMethod("toUpperCase");
System.out.println(m.invoke(str)); // HELLO

89、简述一下面向对象的”六原则一法则”

  • 原则一:单一职责原则,模块需要做到高内聚,一个模块只做一件事请。
  • 原则二:开闭原则,对扩展开放,对修改关闭,也就是说抽象层一旦确定就不能修改,只通过扩展实体类的行为来实现需求,关键步骤为抽象化。
  • 原则三:依赖倒置原则,程序要依赖抽象接口,不要依赖具体实现,高层模块不能依赖低层模块,都需要依赖抽象。
  • 原则四:里氏替换原则,子类可以替换父类被调用。子类是增强父类的能力,而不是减少父类的能力。
  • 原则五:接口隔离原则,类间的依赖建立在最小接口上。也就是接口要小而专,而不能大而全。一个接口表示一种能力,需要高内聚。
  • 原则六:合成/聚合复用原则,优先使用聚合或合成关系复用代码,而不是继承。
  • 法则一:迪米特法则(最小知识原则),一个对象应当对其他对象有尽可能少的了解,即低耦合。

90、简述一下你了解的设计模式。

  • 工厂模式:工厂类可以根据条件生成不同子类实例,这些子类有一个公共的抽象父类并且实现了相同的方法,但是这些方法针对不同的数据进行了不同的操作(多态方法)。当得到子类的实例后,开发人员可以调用基类中的方法而不必考虑到底返回的是哪一个子类的实例。
  • 代理模式:给一个目标对象提供一个代理对象,并由代理对象控制原对象的引用。可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。
  • 适配器模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起使用的类能够一起工作。
  • 模板方法模式:提供一个抽象类,将部分逻辑以具体方法或构造器的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法(多态实现),从而实现不同的业务逻辑。

91. 用 Java 写一个单例类。

饿汉式单例:


public class Singleton {
    private Singleton(){}
    private static Singleton instance = new Singleton();
    public static Singleton getInstance(){
        return instance;
    }
}

懒汉式单例:

public class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static synchronized Singleton getInstance(){
        if (instance == null) instance = new Singleton();
            return instance;
    }
}

注意:实现一个单例有两点注意事项,①将构造器私有,不允许外界通过构造器创建对象;②通过公开的静态方法向外界返回类的唯一实例。

扩展:Spring 的 IoC 容器可以为普通的类创建单例,它是怎么做到的呢?

Spring是通过注册表实现单例的,Ioc容器维护了一个bean表格,当需要一个单例bean时,从表格中获取,没有获取到的,向表格注册一个新的bean。

92. 什么是 UML?

UML 是统一建模语言(Unified Modeling Language)的缩写,它发表于 1997年,综合了当时已经存在的面向对象的建模语言、方法和过程,是一个支持模型化和软件系统开发的图形化语言,为软件开发的所有阶段提供模型化和可视化支持。使用 UML 可以帮助沟通与交流,辅助应用设计和文档的生成,还能够阐释系统的结构和行为。

93. UML 中有哪些常用的图?

UML 定义了多种图形化的符号来描述软件系统部分或全部的静态结构和动态结构,包括:用例图(use case diagram)、类图(class diagram)、时序图(sequencediagram)、协作图(collaboration diagram)、状态图(statechart diagram)、活动图(activity diagram)、构件图(component diagram)、部署图(deploymentdiagram)等。

在这些图形化符号中,有三种图最为重要,分别是:用例图(用来捕获需求,描述系统的功能,通过该图可以迅速的了解系统的功能模块及其关系)、类图(描述类以及类与类之间的关系,通过该图可以快速了解系统)、时序图(描述执行特定任务时对象之间的交互关系以及执行顺序,通过该图可以了解对象能接收的消息也就是说对象能够向外界提供的服务)。

94. 用 Java 写一个冒泡排序

@Override
public <T extends Comparable<T>> void sort(T[] list) {
    boolean swapped = true;
    for (int i = 1, len = list.length; i < len && swapped; ++i) {
        swapped = false;
        for (int j = 0; j < len - i; ++j) {
            if (list[j].compareTo(list[j + 1]) > 0) {
                T temp = list[j];
                list[j] = list[j + 1];
                list[j + 1] = temp;
                swapped = true;
            }
        }
        if (!swapped){
            break;
        }
    }
}

95. 用 Java 写一个折半查找。

折半查找,也称二分查找二分搜索,是一种在有序数组中查找某一特定元素的搜索算法。搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组已经为空,则表示找不到指定的元素。这种搜索算法每一次比较都使搜索范围缩小一半,其时间复杂度是 O(logN)

// 使用循环实现的二分查找
public static <T> int binarySearch(T[] x, T key, Comparator<T> comp) {
    int low = 0;
    int high = x.length - 1;
    while (low <= high) {
        int mid = (low + high) >>> 1;
        int cmp = comp.compare(x[mid], key);
        if (cmp < 0) {
            low = mid + 1;
        } else if (cmp > 0) {
            high = mid - 1;
        } else {
            return mid;
        }
    }
    return -1;
}

// 使用递归实现的二分查找
private static <T extends Comparable<T>> int binarySearch(T[] x, int
                                                          low, int high, T key) {
    if (low <= high) {
        int mid = low + ((high - low) >> 1);
        if (key.compareTo(x[mid]) == 0) {
            return mid;
        } else if (key.compareTo(x[mid]) < 0) {
            return binarySearch(x, low, mid - 1, key);
        } else {
            return binarySearch(x, mid + 1, high, key);
        }
    }
    return -1;
}

96. Java 中能创建 volatile 数组吗?

在java并发编程中有三个特性:

  • 可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。
  • 原子性:指一个操作或多个操作具有原子性时,它必须是要么全部执行,要么全不执行。没有执行一半的说法,也没有被执行一半时被打断的说法。
  • 有序性:一般来说,处理器为了提高程序运行效率会对代码进行优化,进行指令重排操作,这种操作在并发编程中可能会产生问题,所以并发要求有序性,指的是程序执行的顺序按照代码的先后顺序执行。

volatile:是java中的关键字,是Java虚拟机提供的轻量级同步机制。其保证了可见性并禁止指令重排

  • 读取volatile变量时,在该变量之后的变量也将从主存中重新读取(在volatile变量读操作发生之后的变量,因为禁止了指令重排序,所以是可见的)
  • 写入volatile变量时,在该变量之前的变量产生的修改也将写入到主存中(在volatile变量写操作发生之前的变量,因为禁止了指令重排序,所以是可见的)

原理是:在指令之间插入一条内存屏障(Memory Barrier),告诉编译器和CPU,禁止在内存屏障前后的指令执行重新排序,并强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

volatile修饰数组等引用类型,但只保护了引用的可见性,而不保护引用的内容。如修改数组内容将不保证可见,但对数组进行扩容或改变引用的指向,则会刷新内存。

97. volatile 能使得一个非原子操作变成原子操作吗?

能,但不完全能。其能够保证简单操作单个操作的原子性,但对于复合操作仍存在线程不安全的问题,需要使用lock/synchronized

因为volatile只能修饰变量,对于long或string这样的64位长度的变量,读写操作是分为两部分的,先读写前32位,再读写后32位,如果用volatile修饰,则可以一次性完成读写,实现原子性。synchronized可以修饰变量,方法以及代码块,可以保证可见性和原子性。

98. volatile 修饰符的有过什么实践?

用的比较简单,主要是保证了数据的可见性,比如说利用volatile修饰消费券库存,防止抢券出现人数大于券数的问题。

99. volatile 类型变量提供什么保证?

volatile 保证了有序性和可见性。某些情况下,volatile 还能提供原子性,如读 64 位

数据类型,像 long 和 double 都不是原子的,但 volatile 类型的 double 和long 就是原子的。

具体可以参考96,97。

100. 10 个线程和 2 个线程的同步代码,哪个更容易写

从写代码的角度来说,两者的复杂度是相同的,因为同步代码与线程数量是相互独立的。但是同步策略的选择依赖于线程的数量,因为越多的线程意味着更大的竞争,需要利用同步技术,如锁分离,这要求更复杂的代码和专业知识。

posted @ 2022-12-10 22:18  小拳头呀  阅读(32)  评论(0编辑  收藏  举报