java面试大全(二)(基础)

58、线程的sleep()方法和yield()方法有什么区别?
答:
① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
④ sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。

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

60、请说出与线程同步以及线程调度相关的方法。
答:
- wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
- sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
- notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
- notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

提示:关于Java多线程和并发编程的问题,建议大家看我的另一篇文章《关于Java并发编程的总结和思考》

补充:Java 5通过Lock接口提供了显式的锁机制(explicit lock),增强了灵活性以及对线程的协调。Lock接口中定义了加锁(lock())和解锁(unlock())的方法,同时还提供了newCondition()方法来产生用于线程之间通信的Condition对象;此外,Java 5还提供了信号量机制(semaphore),信号量可以用来限制对某个共享资源进行访问的线程的数量。在对资源进行访问之前,线程必须得到信号量的许可(调用Semaphore对象的acquire()方法);在完成对资源的访问后,线程必须向信号量归还许可(调用Semaphore对象的release()方法)。

下面的例子演示了100个线程同时向一个银行账户中存入1元钱,在没有使用同步机制和使用同步机制情况下的执行情况。

  • 银行账户类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
  * 银行账户
  * @author 骆昊
  *
  */
public class Account {
     private double balance;     // 账户余额
 
     /**
      * 存款
      * @param money 存入金额
      */
     public void deposit( double money) {
         double newBalance = balance + money;
         try {
             Thread.sleep( 10 );   // 模拟此业务需要一段处理时间
         }
         catch (InterruptedException ex) {
             ex.printStackTrace();
         }
         balance = newBalance;
     }
 
     /**
      * 获得账户余额
      */
     public double getBalance() {
         return balance;
     }
}
  • 存钱线程类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
  * 存钱线程
  * @author 骆昊
  *
  */
public class AddMoneyThread implements Runnable {
     private Account account;    // 存入账户
     private double money;       // 存入金额
 
     public AddMoneyThread(Account account, double money) {
         this .account = account;
         this .money = money;
     }
 
     @Override
     public void run() {
         account.deposit(money);
     }
 
}
  • 测试类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class Test01 {
 
     public static void main(String[] args) {
         Account account = new Account();
         ExecutorService service = Executors.newFixedThreadPool( 100 );
 
         for ( int i = 1 ; i <= 100 ; i++) {
             service.execute( new AddMoneyThread(account, 1 ));
         }
 
         service.shutdown();
 
         while (!service.isTerminated()) {}
 
         System.out.println( "账户余额: " + account.getBalance());
     }
}

在没有同步的情况下,执行结果通常是显示账户余额在10元以下,出现这种状况的原因是,当一个线程A试图存入1元的时候,另外一个线程B也能够进入存款的方法中,线程B读取到的账户余额仍然是线程A存入1元钱之前的账户余额,因此也是在原来的余额0上面做了加1元的操作,同理线程C也会做类似的事情,所以最后100个线程执行结束时,本来期望账户余额为100元,但实际得到的通常在10元以下(很可能是1元哦)。解决这个问题的办法就是同步,当一个线程对银行账户存钱时,需要将此账户锁定,待其操作完成后才允许其他的线程进行操作,代码有如下几种调整方案:

  • 在银行账户的存款(deposit)方法上同步(synchronized)关键字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
  * 银行账户
  * @author 骆昊
  *
  */
public class Account {
     private double balance;     // 账户余额
 
     /**
      * 存款
      * @param money 存入金额
      */
     public synchronized void deposit( double money) {
         double newBalance = balance + money;
         try {
             Thread.sleep( 10 );   // 模拟此业务需要一段处理时间
         }
         catch (InterruptedException ex) {
             ex.printStackTrace();
         }
         balance = newBalance;
     }
 
     /**
      * 获得账户余额
      */
     public double getBalance() {
         return balance;
     }
}
  • 在线程调用存款方法时对银行账户进行同步
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
  * 存钱线程
  * @author 骆昊
  *
  */
public class AddMoneyThread implements Runnable {
     private Account account;    // 存入账户
     private double money;       // 存入金额
 
     public AddMoneyThread(Account account, double money) {
         this .account = account;
         this .money = money;
     }
 
     @Override
     public void run() {
         synchronized (account) {
             account.deposit(money);
         }
     }
 
}
  • 通过Java 5显示的锁机制,为每个银行账户创建一个锁对象,在存款操作进行加锁和解锁的操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
/**
  * 银行账户
  *
  * @author 骆昊
  *
  */
public class Account {
     private Lock accountLock = new ReentrantLock();
     private double balance; // 账户余额
 
     /**
      * 存款
      *
      * @param money
      *            存入金额
      */
     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();
         }
     }
 
     /**
      * 获得账户余额
      */
     public double getBalance() {
         return balance;
     }
}

按照上述三种方式对代码进行修改后,重写执行测试代码Test01,将看到最终的账户余额为100元。当然也可以使用Semaphore或CountdownLatch来实现同步。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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关键字可以将对象或者方法标记为同步,以实现对对象和方法的互斥访问,可以用synchronized(对象) { … }定义同步代码块,或者在声明方法时将synchronized作为方法的修饰符。在第60题的例子中已经展示了synchronized关键字的用法。

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

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

65、什么是线程池(thread pool)?
答:在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
Java 5+中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类Executors面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:
- newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
- newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
- newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
- newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
- newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

第60题的例子中演示了通过Executors工具类创建线程池并使用线程池执行线程的代码。如果希望在服务器上使用线程池,强烈建议使用newFixedThreadPool方法来创建线程池,这样能获得更好的性能。

66、线程的基本状态以及状态之间的关系?
答:
这里写图片描述

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

67、简述synchronized 和java.util.concurrent.locks.Lock的异同?
答:Lock是Java 5以后引入的新的API,和关键字synchronized相比主要相同点:Lock 能完成synchronized所实现的所有功能;主要不同点:Lock有比synchronized更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且最好在finally 块中释放(这是释放外部资源的最好的地方)。

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

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

面试题 - 编程实现文件拷贝。(这个题目在笔试的时候经常出现,下面的代码给出了两种实现方案)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
 
public final class MyUtil {
 
     private MyUtil() {
         throw new AssertionError();
     }
 
     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();
                 }
             }
         }
     }
}

注意:上面用到Java 7的TWR,使用TWR后可以不用在finally中释放外部资源 ,从而让代码更加优雅。

70、写一个方法,输入一个文件名和一个字符串,统计这个字符串在这个文件中出现的次数。
答:代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.io.BufferedReader;
import java.io.FileReader;
 
public final class MyUtil {
 
     // 工具类中的方法都是静态方式访问的因此将构造器私有不允许创建对象(绝对好习惯)
     private MyUtil() {
         throw new AssertionError();
     }
 
     /**
      * 统计给定文件中给定字符串的出现次数
      *
      * @param filename  文件名
      * @param word 字符串
      * @return 字符串在文件中出现的次数
      */
     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 ;
                 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代码列出一个目录下所有的文件?
答:
如果只要求列出当前文件夹下的文件,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.File;
 
class Test12 {
 
     public static void main(String[] args) {
         File f = new File( "/Users/Hao/Downloads" );
         for (File temp : f.listFiles()) {
             if (temp.isFile()) {
                 System.out.println(temp.getName());
             }
         }
     }
}

如果需要对文件夹继续展开,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.io.File;
 
class Test12 {
 
     public static void main(String[] args) {
         showDirectory( new File( "/Users/Hao/Downloads" ));
     }
 
     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());
         }
     }
}

在Java 7中可以使用NIO.2的API来做同样的事情,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ShowFileTest {
 
     public static void main(String[] args) throws IOException {
         Path initPath = Paths.get( "/Users/Hao/Downloads" );
         Files.walkFileTree(initPath, new SimpleFileVisitor<Path>() {
 
             @Override
             public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                     throws IOException {
                 System.out.println(file.getFileName().toString());
                 return FileVisitResult.CONTINUE;
             }
 
         });
     }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
 
public class EchoServer {
 
     private static final int ECHO_SERVER_PORT = 6789 ;
 
     public static void main(String[] args) {       
         try (ServerSocket server = new ServerSocket(ECHO_SERVER_PORT)) {
             System.out.println( "服务器已经启动..." );
             while ( true ) {
                 Socket client = server.accept();
                 new Thread( new ClientHandler(client)).start();
             }
         } catch (IOException e) {
             e.printStackTrace();
         }
     }
 
     private static class ClientHandler implements Runnable {
         private Socket client;
 
         public ClientHandler(Socket client) {
             this .client = client;
         }
 
         @Override
         public void run() {
             try (BufferedReader br = new BufferedReader( new InputStreamReader(client.getInputStream()));
                     PrintWriter pw = new PrintWriter(client.getOutputStream())) {
                 String msg = br.readLine();
                 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();
                 }
             }
         }
     }
 
}

注意:上面的代码使用了Java 7的TWR语法,由于很多外部资源类都间接的实现了AutoCloseable接口(单方法回调接口),因此可以利用TWR语法在try结束的时候通过回调的方式自动调用外部资源类的close()方法,避免书写冗长的finally代码块。此外,上面的代码用一个静态内部类实现线程的功能,使用多线程可以避免一个用户I/O操作所产生的中断影响其他用户对服务器的访问,简单的说就是一个用户的输入操作不会造成其他用户的阻塞。当然,上面的代码使用线程池可以获得更好的性能,因为频繁的创建和销毁线程所造成的开销也是不可忽视的。

下面是一段回显客户端测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
 
public class EchoClient {
 
     public static void main(String[] args) throws Exception {
         Socket client = new Socket( "localhost" , 6789 );
         Scanner sc = new Scanner(System.in);
         System.out.print( "请输入内容: " );
         String msg = sc.nextLine();
         sc.close();
         PrintWriter pw = new PrintWriter(client.getOutputStream());
         pw.println(msg);
         pw.flush();
         BufferedReader br = new BufferedReader( new InputStreamReader(client.getInputStream()));
         System.out.println(br.readLine());
         client.close();
     }
}

如果希望用NIO的多路复用套接字实现服务器,代码如下所示。NIO的操作虽然带来了更好的性能,但是有些操作是比较底层的,对于初学者来说还是有些难于理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
 
public class EchoServerNIO {
 
     private static final int ECHO_SERVER_PORT = 6789 ;
     private static final int ECHO_SERVER_TIMEOUT = 5000 ;
     private static final int BUFFER_SIZE = 1024 ;
 
     private static ServerSocketChannel serverChannel = null ;
     private static Selector selector = null ;    // 多路复用选择器
     private static ByteBuffer buffer = null ;    // 缓冲区
 
     public static void main(String[] args) {
         init();
         listen();
     }
 
     private static void init() {
         try {
             serverChannel = ServerSocketChannel.open();
             buffer = ByteBuffer.allocate(BUFFER_SIZE);
             serverChannel.socket().bind( new InetSocketAddress(ECHO_SERVER_PORT));
             serverChannel.configureBlocking( false );
             selector = Selector.open();
             serverChannel.register(selector, SelectionKey.OP_ACCEPT);
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
     }
 
     private static void listen() {
         while ( true ) {
             try {
                 if (selector.select(ECHO_SERVER_TIMEOUT) != 0 ) {
                     Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                     while (it.hasNext()) {
                         SelectionKey key = it.next();
                         it.remove();
                         handleKey(key);
                     }
                 }
             } catch (Exception e) {
                 e.printStackTrace();
             }
         }
     }
 
     private static void handleKey(SelectionKey key) throws IOException {
         SocketChannel channel = null ;
 
         try {
             if (key.isAcceptable()) {
                 ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                 channel = serverChannel.accept();
                 channel.configureBlocking( false );
                 channel.register(selector, SelectionKey.OP_READ);
             } else if (key.isReadable()) {
                 channel = (SocketChannel) key.channel();
                 buffer.clear();
                 if (channel.read(buffer) > 0 ) {
                     buffer.flip();
                     CharBuffer charBuffer = CharsetHelper.decode(buffer);
                     String msg = charBuffer.toString();
                     System.out.println( "收到" + channel.getRemoteAddress() + "的消息:" + msg);
                     channel.write(CharsetHelper.encode(CharBuffer.wrap(msg)));
                 } else {
                     channel.close();
                 }
             }
         } catch (Exception e) {
             e.printStackTrace();
             if (channel != null ) {
                 channel.close();
             }
         }
     }
 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
 
public final class CharsetHelper {
     private static final String UTF_8 = "UTF-8" ;
     private static CharsetEncoder encoder = Charset.forName(UTF_8).newEncoder();
     private static CharsetDecoder decoder = Charset.forName(UTF_8).newDecoder();
 
     private CharsetHelper() {
     }
 
     public static ByteBuffer encode(CharBuffer in) throws CharacterCodingException{
         return encoder.encode(in);
     }
 
     public static CharBuffer decode(ByteBuffer in) throws CharacterCodingException{
         return decoder.decode(in);
     }
}

73、XML文档定义有几种形式?它们之间有何本质区别?解析XML文档有哪几种方式?
答:XML文档定义分为DTD和Schema两种形式,二者都是对XML语法的约束,其本质区别在于Schema本身也是一个XML文件,可以被XML解析器解析,而且可以为XML承载的数据定义类型,约束能力较之DTD更强大。对XML的解析主要有DOM(文档对象模型,Document Object Model)、SAX(Simple API for XML)和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操作数据库的步骤。
答:下面的代码以连接本机的Oracle数据库为例,演示JDBC操作数据库的步骤。

  • 加载驱动。
1
Class.forName( "oracle.jdbc.driver.OracleDriver" );
  • 创建连接。
1
Connection con = DriverManager.getConnection( "jdbc:oracle:thin:@localhost:1521:orcl" , "scott" , "tiger" );
  • 创建语句。
1
2
3
PreparedStatement ps = con.prepareStatement( "select * from emp where sal between ? and ?" );
ps.setInt( 1 , 1000 );
ps.setInt( 2 , 3000 );
  • 执行语句。
1
ResultSet rs = ps.executeQuery();
  • 处理结果。
1
2
3
while (rs.next()) {
     System.out.println(rs.getInt( "empno" ) + " - " + rs.getString( "ename" ));
}
  • 关闭资源。
1
2
3
4
5
6
7
8
9
finally {
     if (con != null ) {
         try {
             con.close();
         } catch (SQLException e) {
             e.printStackTrace();
         }
     }
}

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

76、Statement和PreparedStatement有什么区别?哪个性能更好?
答:与Statement相比,①PreparedStatement接口代表预编译的语句,它主要的优势在于可以减少SQL的编译错误并增加SQL的安全性(减少SQL注射攻击的可能性);②PreparedStatement中的SQL语句是可以带参数的,避免了用字符串连接拼接SQL语句的麻烦和不安全;③当批量处理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读取到的数据就是脏数据。

时间 转账事务A 取款事务B
T1   开始事务
T2 开始事务  
T3   查询账户余额为1000元
T4   取出500元余额修改为500元
T5 查询账户余额为500元(脏读)  
T6   撤销事务余额恢复为1000元
T7 汇入100元把余额修改为600元  
T8 提交事务  

不可重复读(Unrepeatable Read):事务A重新读取前面读取过的数据,发现该数据已经被另一个已提交的事务B修改过了。

时间 转账事务A 取款事务B
T1   开始事务
T2 开始事务  
T3   查询账户余额为1000元
T4 查询账户余额为1000元  
T5   取出100元修改余额为900元
T6   提交事务
T7 查询账户余额为900元(不可重复读)  

幻读(Phantom Read):事务A重新执行一个查询,返回一系列符合查询条件的行,发现其中插入了被事务B提交的行。

时间 统计金额事务A 转账事务B
T1   开始事务
T2 开始事务  
T3 统计总存款为10000元  
T4   新增一个存款账户存入100元
T5   提交事务
T6 再次统计总存款为10100元(幻读)  

第1类丢失更新:事务A撤销时,把已经提交的事务B的更新数据覆盖了。

时间 取款事务A 转账事务B
T1 开始事务  
T2   开始事务
T3 查询账户余额为1000元  
T4   查询账户余额为1000元
T5   汇入100元修改余额为1100元
T6   提交事务
T7 取出100元将余额修改为900元  
T8 撤销事务  
T9 余额恢复为1000元(丢失更新)  

第2类丢失更新:事务A覆盖事务B已经提交的数据,造成事务B所做的操作丢失。

时间 转账事务A 取款事务B
T1   开始事务
T2 开始事务  
T3   查询账户余额为1000元
T4 查询账户余额为1000元  
T5   取出100元将余额修改为900元
T6   提交事务
T7 汇入100元将余额修改为1100元  
T8 提交事务  
T9 查询账户余额为1100元(丢失更新)  

数据并发访问所产生的问题,在有些场景下可能是允许的,但是有些场景下可能就是致命的,数据库通常会通过锁机制来解决数据并发访问问题,按锁定对象不同可以分为表级锁和行级锁;按并发事务锁定关系可以分为共享锁和独占锁,具体的内容大家可以自行查阅资料进行了解。
直接使用锁是非常麻烦的,为此数据库为用户提供了自动锁机制,只要用户指定会话的事务隔离级别,数据库就会通过分析SQL语句然后为事务访问的资源加上合适的锁,此外,数据库还会维护这些锁通过各种手段提高系统的性能,这些对用户来说都是透明的(就是说你不用理解,事实上我确实也不知道)。ANSI/ISO SQL 92标准定义了4个等级的事务隔离级别,如下表所示:

隔离级别 脏读 不可重复读 幻读 第一类丢失更新 第二类丢失更新
READ UNCOMMITED 允许 允许 允许 不允许 允许
READ COMMITTED 不允许 允许 允许 不允许 允许
REPEATABLE READ 不允许 不允许 允许 不允许 不允许
SERIALIZABLE 不允许 不允许 不允许 不允许 不允许

需要说明的是,事务隔离级别和数据访问的并发性是对立的,事务隔离级别越高并发性就越差。所以要根据具体的应用来确定合适的事务隔离级别,这个地方没有万能的原则。

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的PreparedStatement和ResultSet都提供了相应的方法来支持Blob和Clob操作。下面的代码展示了如何使用JDBC操作LOB:
下面以MySQL数据库为例,创建一个张有三个字段的用户表,包括编号(id)、姓名(name)和照片(photo),建表语句如下:

1
2
3
4
5
6
create table tb_user
(
id int primary key auto_increment,
name varchar( 20 ) unique not null ,
photo longblob
);

下面的Java代码向数据库中插入一条记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
 
class JdbcLobTest {
 
     public static void main(String[] args) {
         Connection con = null ;
         try {
             // 1. 加载驱动(Java6以上版本可以省略)
             Class.forName( "com.mysql.jdbc.Driver" );
             // 2. 建立连接
             con = DriverManager.getConnection( "jdbc:mysql://localhost:3306/test" , "root" , "123456" );
             // 3. 创建语句对象
             PreparedStatement ps = con.prepareStatement( "insert into tb_user values (default, ?, ?)" );
             ps.setString( 1 , "骆昊" );              // 将SQL语句中第一个占位符换成字符串
             try (InputStream in = new FileInputStream( "test.jpg" )) {    // Java 7的TWR
                 ps.setBinaryStream( 2 , in);      // 将SQL语句中第二个占位符换成二进制流
                 // 4. 发出SQL语句获得受影响行数
                 System.out.println(ps.executeUpdate() == 1 ? "插入成功" : "插入失败" );
             } catch (IOException e) {
                 System.out.println( "读取照片失败!" );
             }
         } catch (ClassNotFoundException | SQLException e) {     // Java 7的多异常捕获
             e.printStackTrace();
         } finally { // 释放外部资源的代码都应当放在finally中保证其能够得到执行
             try {
                 if (con != null && !con.isClosed()) {
                     con.close();    // 5. 释放数据库连接
                     con = null ;     // 指示垃圾回收器可以回收该对象
                 }
             } catch (SQLException e) {
                 e.printStackTrace();
             }
         }
     }
}

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
class RegExpTest {
 
     public static void main(String[] args) {
         String str = "北京市(朝阳区)(西城区)(海淀区)" ;
         Pattern p = Pattern.compile( ".*?(?=\\()" );
         Matcher m = p.matcher(str);
         if (m.find()) {
             System.out.println(m.group());
         }
     }
}

说明:上面的正则表达式中使用了懒惰匹配和前瞻,如果不清楚这些内容,推荐读一下网上很有名的《正则表达式30分钟入门教程》

85、获得一个类的类对象有哪些方式?
答:
- 方法1:类型.class,例如:String.class
- 方法2:对象.getClass(),例如:”hello”.getClass()
- 方法3:Class.forName(),例如:Class.forName(“java.lang.String”)

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

87、如何通过反射获取和设置对象私有字段的值?
答:可以通过类对象的getDeclaredField()方法字段(Field)对象,然后再通过字段对象的setAccessible(true)将其设置为可以访问,接下来就可以通过get/set方法来获取/设置字段的值了。下面的代码实现了一个反射的工具类,其中的两个静态方法分别用于获取和设置私有字段的值,字段可以是基本类型也可以是对象类型且支持多级对象操作,例如ReflectionUtil.get(dog, “owner.car.engine.id”);可以获得dog对象的主人的汽车的引擎的ID号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
 
/**
  * 反射工具类
  * @author 骆昊
  *
  */
public class ReflectionUtil {
 
     private ReflectionUtil() {
         throw new AssertionError();
     }
 
     /**
      * 通过反射取对象指定字段(属性)的值
      * @param target 目标对象
      * @param fieldName 字段的名字
      * @throws 如果取不到对象指定字段的值则抛出异常
      * @return 字段的值
      */
     public static Object getValue(Object target, String fieldName) {
         Class<?> clazz = target.getClass();
         String[] fs = fieldName.split( "\\." );
 
         try {
             for ( int i = 0 ; i < fs.length - 1 ; i++) {
                 Field f = clazz.getDeclaredField(fs[i]);
                 f.setAccessible( true );
                 target = f.get(target);
                 clazz = target.getClass();
             }
 
             Field f = clazz.getDeclaredField(fs[fs.length - 1 ]);
             f.setAccessible( true );
             return f.get(target);
         }
         catch (Exception e) {
             throw new RuntimeException(e);
         }
     }
 
     /**
      * 通过反射给对象的指定字段赋值
      * @param target 目标对象
      * @param fieldName 字段的名称
      * @param value 值
      */
     public static void setValue(Object target, String fieldName, Object value) {
         Class<?> clazz = target.getClass();
         String[] fs = fieldName.split( "\\." );
         try {
             for ( int i = 0 ; i < fs.length - 1 ; i++) {
                 Field f = clazz.getDeclaredField(fs[i]);
                 f.setAccessible( true );
                 Object val = f.get(target);
                 if (val == null ) {
                     Constructor<?> c = f.getType().getDeclaredConstructor();
                     c.setAccessible( true );
                     val = c.newInstance();
                     f.set(target, val);
                 }
                 target = val;
                 clazz = target.getClass();
             }
 
             Field f = clazz.getDeclaredField(fs[fs.length - 1 ]);
             f.setAccessible( true );
             f.set(target, value);
         }
         catch (Exception e) {
             throw new RuntimeException(e);
         }
     }
 
}

88、如何通过反射调用对象的方法?
答:请看下面的代码:

1
2
3
4
5
6
7
8
9
10
import java.lang.reflect.Method;
 
class MethodInvokeTest {
 
     public static void main(String[] args) throws Exception {
         String str = "hello" ;
         Method m = str.getClass().getMethod( "toUpperCase" );
         System.out.println(m.invoke(str));  // HELLO
     }
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
2023年的Java面试大全将包括以下主题: 1. Java基础知识:面试官可能会问及Java的基本概念,如面向对象编程、异常处理、集合框架、多线程编程等。考察候选人对Java语言的了解和基础知识的牢固程度。 2. 数据库和SQL:Java开发常常需要与数据库进行交互,因此面试官可能会询问关于数据库的问题,如SQL语句编写、常用数据库操作等。 3. Spring框架:Spring是Java开发中常用的框架,面试官可能会问及有关Spring的知识,包括IOC容器、AOP编程、Spring MVC等。 4. Web开发:Java开发常用于Web应用程序,面试官可能会问及Servlet、JSP、Web前端技术(HTML、CSS、JavaScript)等相关知识。 5. 设计模式:面试官可能会问及常用的设计模式,如单例模式、工厂模式、观察者模式等。这些模式在Java开发中经常用到,候选人需要对其有一定的了解和运用能力。 6. 数据结构和算法:面试官可能会在面试中涉及到数据结构和算法的问题,如链表、栈、队列等常见数据结构,以及排序算法、查找算法等。这些问题考察候选人的编程能力和对算法的理解。 7. 多线程编程:Java具有良好的多线程支持,面试官可能会问及多线程编程相关的问题,如线程同步机制、线程池、死锁等。 8. 性能优化:Java程序的性能优化是一个重要的话题,面试官可能会问及如何进行性能优化,如内存管理、代码优化等方面的知识。 9. 测试和调试:面试官可能会问及测试和调试相关的问题,如单元测试、集成测试、调试技巧等。 10.项目经验:除了理论知识,面试官可能会询问候选人的项目经验,以了解其实际应用能力和解决问题的能力。 因为是2023年的Java面试大全,可以预计会有更多与Java相关的新技术、框架和工具出现在面试中,候选人应该持续学习和关注最新的Java发展动向,做好充分的准备。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值