【误区】Java基础中常见的误区2

Java基础,庞大而繁杂,这里继续记录一下常见的误区。

目录

1、Java线程中的start()方法和run()方法有何区别?

2、Java中产生随机数的方式

3、Java的File类中路径、绝对路径和相对路径怎么区分的?

4、字符串中+号重载操作的问题

5、异常信息打印中printStackTrace()和fillInStackTrace()有什么区别?


1、Java线程中的start()方法和run()方法有何区别?

简而言之:start()是线程启动的方法,run()是线程运行的方法。

源码角度分析:

  • start()是一个同步方法,采用synchronized是为了防止已创建的线程被多次启动。该启动的线程不会马上运行,会放到等待队列中等待 CPU 调度,只有线程真正被 CPU 调度时才会调用 run() 方法执行;
public synchronized void start() {
    // 线程状态为0,表示线程处于新建状态
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    // 加入线程组
    group.add(this);
    boolean started = false;
    try {
        start0(); // 调用本地线程启动方法
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}
  • Thread 类也是实现了 Runnable 接口,而 Runnable 接口定义了唯一的一个 run() 方法,所以基于 Thread 和 Runnable 创建多线程都需要实现 run() 方法,它是多线程真正运行的主方法。执行run()方法取决于CPU的调度,调用run()方法时只会在原来创建的线程中调用,不会启动新的线程。
public class Thread implements Runnable {
    @Override
    public void run() {
        if (target != null) { // 目标线程不能为null
            target.run(); 
        }
    }
}
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

2、Java中产生随机数的方式

第一种:使用java.util.Random类来产生一个随机数,构造函数有两个 —— Random() 和 Random(long seed)。

public class Test {
    public static void main(String[] args) {
        // 无参
        Random random = new Random();
        System.out.println(random.nextInt(100)); // 随机产生一个0~99的int类型的数值,每次结果都不同
        System.out.println(random.nextDouble()); // 随机产生一个0.0~1.0的double类型的数值,,每次结果都不同
        // 含有long seed,该seed是伪随机数生成器内部状态的初始值,会使得每次随机测试都为第一次出现的值
        Random random1 = new Random(2);
        System.out.println(random1.nextInt(100)); // 第一次随机为8,继续N次测试结果仍然是8
        System.out.println(random1.nextDouble()); // 第一次随机为0.2933765635110084,继续N次测试结果仍然是这个值
    }
}

第二种:使用Math类的静态方法random()来产生一个随机数。本质上还是通过java.util.Random类实现的,源码如下:

public final class Math {
    /** 随机数生成器 */
    private static final class RandomNumberGeneratorHolder {
        static final Random randomNumberGenerator = new Random();
    }
    /** 静态随机数方法 ,调用Random类中的nextDouble()方法*/
    public static double random() {
        return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
    }
}

第三种:多线程环境中,生产随机数可以使用ThreadLocalRandom类中的静态方法current(),ThreadLocalRandom类的构造器是私有的,不能通过构造实例去使用它。

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(
                () -> System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalRandom.current().nextInt(100))
        );
        Thread t2 = new Thread(
                () -> System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalRandom.current().nextDouble(10.0))
        );
        t1.start();
        t2.start();
    }
// Thread-0:88
// Thread-1:6.564094821301532
}

3、Java的File类中路径、绝对路径和相对路径怎么区分的?

File类中通过getPath方法、getAbsolutePath方法、getCanonicalPath方法分别获取的是文件路径、绝对路径、相对路径,那么有何区别呢?测试如下:

①. 当文件是绝对路径时

File file3 = File.createTempFile("2020-12-09-", ".log"
        , new File("E:\\test\\filetest"));
logger.info("------getPath------》" + file3.getPath());
logger.info("------getAbsolutePath------》" + file3.getAbsolutePath());
logger.info("------getCanonicalPath------》" + file3.getCanonicalPath());

测试结果:全部为绝对路径,如图所示。

②. 当文件是相对路径.时

File file4 = File.createTempFile("yesterday", ".log", new File(".\\src\\main\\resources\\templates"));
logger.info("------getPath------》" + file4.getPath());
logger.info("------getAbsolutePath------》" + file4.getAbsolutePath());
logger.info("------getCanonicalPath------》" + file4.getCanonicalPath());
logger.info("------getParent------》" + file4.getParent());

测试结果如下:

  • getPath方法获取的构造器指定的路径;
  • getAbsolutePath方法获取的是user.id + getPath方法的结果;
  • getCanonicalPath方法则是在getAbsolutePath方法基础上做了路径的规范,去除.。

③. 当文件是相对路径..时

createTempFile方法中不能指定..类型的相对路径,会报错:

java.io.IOException: 系统找不到指定的路径。
at java.io.WinNTFileSystem.createFileExclusively(Native Method)
......

采用File类的构造器测试:

// 全路径为:E:\test\filetest\abc\rs.txt
File file5 = new File("..\\abc\\rs.txt");
logger.info("------getPath------》" + file5.getPath());
logger.info("------getAbsolutePath------》" + file5.getAbsolutePath());
logger.info("------getCanonicalPath------》" + file5.getCanonicalPath());
logger.info("------getParent------》" + file5.getParent());

测试结果如下:

  • getPath方法获取的构造器指定的路径;
  • getAbsolutePath方法获取的是user.id + getPath方法的结果;
  • getCanonicalPath方法则是在getAbsolutePath方法基础上做了路径的规范,去除..。

4、字符串中+号重载操作的问题

Java中只允许对String类的+或+=进行重载操作,有以下两种常见的操作符重载类型,它们的比较结果有所不同。

第一种字符串常量与字符串常量的拼接

public class Demo{
   public static void main(String[] args) {
     String str1 = "a" + "b" + "c";
     String str2 = "abc";
     System.out.println(str1 == str2); // true
     System.out.println(str1.equals(str2)); // true
   }
}

分析:str1的"a" 、 "b" 、 "c"进行+符号拼接时,通过Java中常量优化机制,str1结果变成了字符串常量“abc”,常量池中会创建一个“abc”的字符串常量对象,栈上的引用str1会指向它;当执行String str2 = "abc"时,常量池已经存在字符串常量“abc”,不会重新创建对象了,栈上的引用str2也会指向它,因此str1和str2指向的内存地址是一样的,内容都是“abc”。

第二种字符串常量引用与字符串常量的拼接

public class stringTest {
   public static void main(String[] args) {
     String str1 = "ab";
     String str2 = "abc";
     String str3 = str1 + "c";
     System.out.println(str2 == str3);  // false 
     System.out.println(str2.equals(str3));  // true 
   }
}

分析:一开始会在常量池中创建“ab”、“abc”两个对象,str1指向“ab”,str2指向“abc”。通过javap命令去反编译生成的class文件,我们通过反编译的信息能看到,执行String str3 = str1 + "c"大致可分为以下几步:①. 创建一个StringBuilder对象,通过其append()追加字符串"c" ; ②. 拼接完成后通过toString()将StringBuilder对象转换为String对象,该对象会被存储到堆上并将引用str3指向它,该对象内容为“abc”。因此,str2与str3的对象内容是一样的,但在内存中的地址不一样。

5、异常信息打印中printStackTrace()和fillInStackTrace()有什么区别?

printStackTrace() 和 fillInStackTrace() 都是Throwable类的用于打印堆栈异常信息的,先看下源码怎么定义它们的:

public class Throwable implements Serializable {
     private transient Object backtrace;
     private static final StackTraceElement[] UNASSIGNED_STACK = new StackTraceElement[0];
     private StackTraceElement[] stackTrace = UNASSIGNED_STACK;

     /**
     * fillInStackTrace()是一个同步和本地方法,
     *   在Throwable对象中记录当前线程执行的桢栈追踪的信息。
     **/
     public synchronized Throwable fillInStackTrace() {
        // 在Throwable构造器中设置writableStackTrace为false时,stackTrace才为null。
        // if判断意义:让该方法从调用位置替换掉原来的桢栈追踪的信息,不会再重新
        //       填充桢栈追踪的信息,这样避免了因分配栈而带来的内存消耗。
        if (stackTrace != null ||
            backtrace != null /* Out of protocol state */ ) {
            fillInStackTrace(0);
            stackTrace = UNASSIGNED_STACK;
        }
        return this;
    }
    private native Throwable fillInStackTrace(int dummy);

    /**
     *  printStackTrace()方法:打印桢栈中的异常消息
    **/
    public void printStackTrace() {
        printStackTrace(System.err);
    }
    public void printStackTrace(PrintStream s) {
        printStackTrace(new WrappedPrintStream(s));
    }
    private void printStackTrace(PrintStreamOrWriter s) {
        ...
    }
}

看着还是有点糊涂,不如做下测试:(测试代码中调用顺序:testThrowable() -> m1() -> m2() -> m3())

@SpringBootTest
class MySportHealthyApplicationTests {
    Logger logger = LoggerFactory.getLogger(MySportHealthyApplicationTests.class);

    public void m1() {
        Throwable th = new Throwable();
        m2(th);
        logger.error("-----m1()中printStackTrace的异常信息-------->");
        th.printStackTrace();
        logger.error("-----m1()中fillInStackTrace方法的异常消息-------->",th.fillInStackTrace());
    }
    
    public void m2(Throwable th) {
        m3(th);
        logger.error("-----m2()中printStackTrace方法的异常消息-------->");
        th.printStackTrace();
        logger.error("-----m2()中fillInStackTrace方法的异常消息-------->",th.fillInStackTrace());
    }
    
    public void m3(Throwable th) {
        logger.error("-----m3()中printStackTrace方法的异常消息-------->");
        th.printStackTrace();
        logger.error("-----m3()中fillInStackTrace方法的异常消息-------->",th.fillInStackTrace());
    }
    
    @Test
    void testThrowable()  {
        m1();
    }
}

只使用printStackTrace()打印异常信息:

只使用fillInStackTrace()打印异常信息:

同时使用printStackTrace()和fillInStackTrace()打印异常信息:

经过一番测试之后,这里好像可以得到结论了,区别如下:

  • 测试代码调用顺序为:testThrowable() -> m1() -> m2() -> m3(),当代码执行到m3()方法时,fillInStackTrace()打印了m3、m2、m1及testThrowable,当代码执行到m2()方法时,fillInStackTrace()打印了m2、m1及testThrowable,以此类推.....,可以看出:fillInStackTrace()从调用位置处打印栈桢中追踪信息,结合源码的if判断可知,为了性能,当栈桢追踪的信息存在时,下次不再进行重新填充栈桢追踪信息,而新的栈桢追踪直接指向原有的即可
  • printStackTrace()则是从创建异常处打印栈桢追踪信息,与调用位置无关

持续整理中......

猜你喜欢

转载自blog.csdn.net/qq_29119581/article/details/112896162