程序员最近都爱上了这个网站  程序员们快来瞅瞅吧!  it98k网:it98k.com

本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

Javase阶段面试题

发布于2020-11-19 20:33     阅读(1256)     评论(0)     点赞(8)     收藏(1)


1.浅谈八大基本数据类型

类型字节比特位取值范围包装类
byte18

-128(-2 ^ 7) ~ 127(2 ^ 7 - 1)

Byte
short216

-32768(-2 ^ 15) ~ 32767(2 ^ 15 - 1)

Short
int432

(-2 ^ 31) ~ (2 ^ 31 - 1)

Integer
long864

(-2 ^ 63) ~ (2 ^ 63 - 1)

Long
float432

负数范围:-3.4028235E+38 ~ -1.4E-45

正数范围:1.4E-45 ~ 3.4028235E+38

Float
double864

负数范围:-1.7976931348623157E+308 ~ -4.9E-324

正数范围:4.9E-324 ~ 1.7976931348623157E+308

Double

char

216'\u0000' ~ '\uffff'(Unicode码) / 0 ~ 65535(ASCII码)Character
boolean//true / falseBoolean

2.逻辑运算符

& 单与 两边都要计算

true & true = truetrue & false = false
false & true = falsefalse & false = false

| 单或 两边都要计算

true & true = truetrue | false = true
false | true = truefalse & false = false

^ 异或 
相同为0,不同为1

true ^ true = falsetrue ^ false = true
false ^ true = truefalse ^ false = false

! 非
&& 短路与     || 短路或 

区分& 与 &&
相同点1:& 与 && 的运算结果相同
相同点2:当符号左边是true时,二者都会执行符号右边的运算。
不同点:  当符号左边是false时,&继续执行符号右边的运算,而&&不再执行符号右边的运算。

区分:| 与 ||
相同点1:| 与 || 的运算结果相同
相同点2:当符号左边是false时,二者都会执行符号右边的运算。
不同点:  当符号左边是true时,| 继续执行符号右边的运算,而||不再执行符号右边的运算。

3.详谈Object类中的方法

  • boolean equals(Object obj):指示其他某个对象是否与此对象"相等"。

equals()方法不能用来比较基本数据类型,只能比较引用数据类型。

Object类中equals()的定义:
public boolean equals(Object obj) {
      return (this == obj);
}

PS:Object类中定义的equals()方法和==的作用是相同的:都比较的是两个对象的地址值是否相同,即两个引用是否指向同一个对象实体。

像String、Date、File、包装类等都重写了Object类中的equals()方法。重写以后,比较的就不是两个引用的地址是否相同,而是比较两个对象的"实体内容"是否相同。

通常情况下,我们对于自定义的类,一般都会去重写它的equals()方法,从而来比较两个对象的"实体内容"是否相同。

  • String toString():返回该对象的字符串表示。

当我们输出一个对象的引用时,实际上就是调用当前对象的toString()方法。

Object类中toString()的定义:
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
这打印的是当前这个对象的地址值。

像String、Date、File、包装类等都重写了Object类中的toString()方法。重写以后,打印的就不是地址值,而是对象的具体实体内容。

对于自定义的类,一般也都会去重写它的toString()方法,用来返回对象的"实体内容"。

  • Class<?> getClass():返回此Object的运行时类。

先来谈一下类的加载过程:程序经过javac.exe命令以后,会生成一个或多个字节码文件。接着我们使用java.exe命令对某个字节码文件进行解释运行。相当于将某个字节码文件加载到内存中。此过程就称为类的加载。加载到内存中的类,我们就称为运行时类,此运行时类,就作为Class类的一个实例。即Class的实例就对应着一个运行时类。加载到内存中的运行时类,会缓存一定的时间。在此时间之内,我们可以通过不同的方式来获取此运行时类。getClass()就是其中的一种方式。

顺便在这里说一下获取运行时类的四种方式:

  1. //方式一:调用运行时类的属性:.class
  2. Class clazz1 = 当前类.class;
  3. System.out.println(clazz1);
  4. //方式二:通过运行时类的对象,调用getClass()
  5. 当前类 p1 = new 当前类();
  6. Class clazz2 = p1.getClass();
  7. System.out.println(clazz2);
  8. //方式三:调用Class的静态方法:forName(String classPath)
  9. Class clazz3 = Class.forName("com.self.java.当前类");
  10. System.out.println(clazz3);
  11. //方式四:使用类的加载器:ClassLoader
  12. ClassLoader classLoader = 当前类.class.getClassLoader();
  13. Class clazz4 = classLoader.loadClass("com.self.java.当前类");
  14. System.out.println(clazz4);
  • protected void finalize():当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
  • void wait():在其他线程调用此对象的notify()方法或notifyAll()方法前,导致当前线程等待。
  • void wait(long timeout):在其他线程调用此对象的notify()方法或notifyAll()方法,或者超过指定的时间量前,导致当前线程等待。 
  • void wait(long timeout,int nanos):在其他线程调用此对象的notify()方法或notifyAll()方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。 

这三个wait()方法构成了重载,但是用法基本是一样的。一旦执行wait()方法,当前线程就进入阻塞状态,并随之释放同步监视器。

  • void notify():唤醒在此对象监视器上等待的单个线程。

执行此方法,就会唤醒被阻塞的一个线程。如果有多个线程被阻塞,就唤醒优先级高的那个。

  • void notifyAll():唤醒在此对象监视器上等待的所有线程。

执行此方法,就会唤醒所有被阻塞的线程。

  • int hashcode():返回该对象的哈希值。

hashCode()方法在某种程度上提高了equals()的比较效率。比如一个set集合中有10000个字符串,如果没有hashCode()方法,每次add()一个元素都调用equals()方法进行一个一个的比较的话,效率太低。但是有了hashCode方法之后,会先比较它们的哈希值是否相等,如果相等,再利用equals()方法来进行比较,不相等的话,就省去用equals()来进行比较这一步了。

问题:为什么重写了equals()方法之后还要再重写hashcode()方法?

针对这个问题,我们从两个方面来说:

  1. 第一个是效率

   如上面所说,比如我们要往set集合中添加数据,因为set是不能存储重复元素的,所以我们每次添加新元素前都要先判断这个元素在不在集合里面。如果不重写hashCode()方法,每次add()一个元素都要调用equals()方法一个一个进行比较,这样效率太低。但是重写了hashCode方法之后,就可以先比较它们的哈希码值是否相等,如果相等,再利用equals()方法来进行比较,不相等的话,就省去用equals()来进行比较这一步了,能节省很多时间。

     2. 保证是同一个对象

         如果重写了equals()方法,而没有重写hashcode()方法,就会出现用equals()比较相等、但是哈希值不相等的对象,重写hashcode()方法就是为了避免这种情况的出现。

PS:如果两个对象相同,那么它们的hashCode值一定相同;反之如果两个对象的哈希值相同,它们不一定相同。

  • protected Object clone():创建并返回此对象的一个副本。

提到clone()方法,这里就要谈一下深拷贝和浅拷贝。

浅拷贝:使用clone()方法来实现。

  1. 对于基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。 
  2. 对于引用数据类型的成员变量,浅拷贝会进行引用传递,也就是将该成员变量的在内存当中的地址复制一份给新的对象。

         这种情况下,修改一个对象中的成员变量,也会影响到另一个对象的该成员变量值 。

深拷贝:有两种实现方式,一是重写clone()方法来实现深拷贝,二是通过对象序列化实现深拷贝。

  1. 对于基本数据类型的成员变量,深拷贝会复制该对象的所有基本数据类型的成员变量值给新的对象。
  2. 对于引用数据类型的成员变量,深拷贝会为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象(包括对象的引用类型)进行拷贝。

关于深拷贝和浅拷贝有个例子

  1. package com.self.sa.copy.deep;
  2. import java.io.Serializable;
  3. import java.util.Objects;
  4. public class DeepCloneableTarget implements Serializable, Cloneable {
  5. private static final long serialVersionUID = 1L;
  6. private String cloneName;
  7. private String cloneClass;
  8. public DeepCloneableTarget(String cloneName, String cloneClass) {
  9. this.cloneName = cloneName;
  10. this.cloneClass = cloneClass;
  11. }
  12. @Override
  13. protected Object clone() throws CloneNotSupportedException {
  14. return super.clone();
  15. }
  16. @Override
  17. public boolean equals(Object o) {
  18. if (this == o) return true;
  19. if (o == null || getClass() != o.getClass()) return false;
  20. DeepCloneableTarget that = (DeepCloneableTarget) o;
  21. return Objects.equals(cloneName, that.cloneName) &&
  22. Objects.equals(cloneClass, that.cloneClass);
  23. }
  24. @Override
  25. public int hashCode() {
  26. return Objects.hash(cloneName, cloneClass);
  27. }
  28. }
  1. package com.self.sa.copy.deep;
  2. import java.io.*;
  3. import java.util.Objects;
  4. public class DeepProtoType implements Serializable, Cloneable {
  5. public String name; //String 属性
  6. public DeepCloneableTarget deepCloneableTarget; //引用类型
  7. public DeepProtoType() {}
  8. //深拷贝-方式1 使用clone()方法
  9. @Override
  10. protected Object clone() throws CloneNotSupportedException {
  11. Object deep = null;
  12. //这里完成对基本数据类型属性和String的克隆
  13. deep = super.clone();
  14. //对引用类型的属性进行单独处理
  15. DeepProtoType deepProtoType = (DeepProtoType) deep;
  16. deepProtoType.deepCloneableTarget = (DeepCloneableTarget) deepCloneableTarget.clone();
  17. return deepProtoType;
  18. }
  19. //深拷贝-方式2 通过对象的序列化实现(推荐)
  20. public Object deepClone() {
  21. //创建流对象
  22. ByteArrayOutputStream bos = null;
  23. ObjectOutputStream oos = null;
  24. ByteArrayInputStream bis = null;
  25. ObjectInputStream ois = null;
  26. try {
  27. //序列化
  28. bos = new ByteArrayOutputStream();
  29. //把字节流转为对象流
  30. oos = new ObjectOutputStream(bos);
  31. oos.writeObject(this); //当前这个对象以对象流的方式输出
  32. //反序列化
  33. bis = new ByteArrayInputStream(bos.toByteArray());
  34. ois = new ObjectInputStream(bis);
  35. DeepProtoType copy = (DeepProtoType) ois.readObject();
  36. return copy;
  37. } catch (Exception e) {
  38. e.printStackTrace();
  39. return null;
  40. } finally {
  41. try {
  42. bos.close();
  43. oos.close();
  44. bis.close();
  45. ois.close();
  46. } catch (Exception e) {
  47. e.printStackTrace();
  48. }
  49. }
  50. }
  51. @Override
  52. public boolean equals(Object o) {
  53. if (this == o) return true;
  54. if (o == null || getClass() != o.getClass()) return false;
  55. DeepProtoType that = (DeepProtoType) o;
  56. return Objects.equals(name, that.name) &&
  57. Objects.equals(deepCloneableTarget, that.deepCloneableTarget);
  58. }
  59. @Override
  60. public int hashCode() {
  61. return Objects.hash(name, deepCloneableTarget);
  62. }
  63. }
  1. package com.self.sa.copy.deep;
  2. public class Client {
  3. public static void main(String[] args) throws CloneNotSupportedException {
  4. DeepProtoType p = new DeepProtoType();
  5. p.name = "哈哈";
  6. p.deepCloneableTarget = new DeepCloneableTarget("大牛", "牛大");
  7. //方式1 完成深拷贝
  8. DeepProtoType p1 = (DeepProtoType) p.clone();
  9. System.out.println(p.deepCloneableTarget == p1.deepCloneableTarget);//false
  10. System.out.println(p.equals(p1));//true
  11. //方式2 完成深拷贝
  12. DeepProtoType p2 = (DeepProtoType) p.deepClone();
  13. System.out.println(p.deepCloneableTarget.hashCode() == p2.deepCloneableTarget.hashCode());//true
  14. System.out.println(p == p2);//true
  15. System.out.println(p.equals(p2));//true
  16. }
  17. }

4.基本数据类型、包装类、String之间的相互转换

PS:关于Integer再说一下: 

  1. Integer a = 128;
  2. Integer b = 128;
  3. System.out.println(a==b);//false

Integer的范围是-128 ~ 127,超出这个范围就会重新创建对象。

5.值传递和引用传递

首先说一下,Java参数传递机制只有值传递,没有引用传递。

  • 如果参数是基本数据类型,此时实参赋给形参的是实参真实存储的数据值。
  • 如果参数是引用数据类型,此时实参赋给形参的是实参存储数据的地址值。

推广一下:

  • 如果变量是基本数据类型,此时赋值的是变量所保存的数据值。
  • 如果变量是引用数据类型,此时赋值的是变量所保存的数据的地址值。

顺便再提一下引用传递,引用传递是针对对象而言,传递的是地址,修改地址会改变原对象。但是对于String类型,是值传递,因为String不可变。

6.String为什么被设计为不可变?String不可变体现在哪里?

一、首先,关于String类我们知道,String底层使用了char型数组,而且被final修饰,所以不可变。

不可变总的来说有以下几个原因:

(1)便于实现字符串常量池

在Java中,我们会经常大量的使用String常量,如果每声明一个String都创建一个String对象,将会造成极大的空间资源的浪费。所以Java提出了字符串常量池的概念,在堆中开辟一块存储空间表示字符串常量池,当我们初始化一个String变量时,如果该字符串已经存在了,会直接拿到该字符串的引用,而不会去创建一个新的字符串常量。如果String是可变的,那么当某一个字符串变量改变它的值,其在字符串常量池的值也会发生改变,这样字符串常量池将不能够实现!

(2)加快字符串的处理速度

由于String是不可变的,这样就保证了每个字符串hashcode的唯一性。当我们在创建String对象时,其hashcode就已经确定了,被缓存下来,就不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象,所以HashMap中的键往往都使用String。

(3)保证了安全性

在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争;但当多个线程对资源进行写操作的时候却是不安全的,由于String设计为不可变,所以保证了多线程的安全。

二、String不可变主要体现在以下几个方面:

(1)当对字符串重新赋值时,

(2)当对字符串进行拼接时,

(3)当用replace()方法修改指定位置的字符串时,

(1)、(2)、(3)三种情况的处理方法都是一样的,都会在堆当中重新开辟内存空间,然后从字符串常量池拿到新的值的地址给原对象,而不是在原字符串上进行修改。

关于字符串常量池这里再提一些内容:

(1)jdk 1.6及以前:字符串常量池存储在方法区(永久区),到jdk 1.7及之后:字符串常量池存储在堆空间。

(2)字符串常量池不会存储相同内容的字符串,它底层使用Hashtable(数组+链表) ;

(3)并且容量是60013。途径是:运行下面代码,然后在命令行输入jps,再输入jinfo -flag StringTableSize+进程id,就可以看到字符串常量池的容量。

  1. package com.self.sa.String;
  2. /**
  3. * 测试StringTableSize
  4. */
  5. public class StringTableTest {
  6. public static void main(String[] args) {
  7. //测试StringTableSize参数
  8. System.out.println("hello");
  9. try {
  10. Thread.sleep(1000000);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. }

效果如下图: 

7.String、StringBuffer、StringBuilder三者异同

String:不可变的字符序列;底层使用char[ ]存储。
StringBuffer:可变的字符序列;线程安全的,效率低;底层使用char[ ]存储,默认容量为16。
StringBuilder:可变的字符序列;jdk5.0新增的,线程不安全的,效率高;底层使用char[ ]存储,默认容量为16。

关于扩容问题:如果要添加的数据过多,底层数组盛不下了,那就需要扩容底层的数组。 默认情况下,扩容的长度为原来容量的2倍 + 2,同时将原数组中的元素复制到新的数组中。

String、StringBuffer、StringBuilder三者的执行效率:从高到低排列为StringBuilder > StringBuffer > String。所以开发中一般建议用StringBuilder。

8.关于字符串的拼接问题

(1)常量与常量的拼接结果在常量池,这是因为存在编译期优化。所谓编译期优化就是指如果两个字符串都是常量,并进行拼接的话,在编译的时候会自动将它们进行拼接。

(2)只要有一个是变量,结果就在堆中,因为会在堆当中重新new String()对象。

(3)如果拼接的结果调用intern()方法,返回值就在常量池中。并且会主动将常量池中还没有的字符串常量放入池中,并返回此对象地址。

详细看以下代码,代码注释当中都有详细说明

  1. /**
  2. * intern()方法演示
  3. */
  4. @Test
  5. public void test2(){
  6. String s1 = "javaEE";
  7. String s2 = "hadoop";
  8. String s3 = "javaEEhadoop";
  9. String s4 = "javaEE" + "hadoop";//因为是两个常量,所以会执行编译期优化,即s4就是"javaEEhadoop"。
  10. //如果拼接符号的前后出现了变量,则会在堆空间中new String()。
  11. String s5 = s1 + "hadoop";//出现了s1变量
  12. String s6 = "javaEE" + s2;//s2也是变量
  13. String s7 = s1 + s2;//两个变量拼接
  14. // s4因为编译期优化,拼接结果就相当于s3
  15. // 至于s5,s6,s7,由于拼接过程中都存在变量,所以都会在堆当中new String()。
  16. System.out.println(s3 == s4);//true
  17. System.out.println(s3 == s5);//false
  18. System.out.println(s3 == s6);//false
  19. System.out.println(s3 == s7);//false
  20. System.out.println(s5 == s6);//false
  21. System.out.println(s5 == s7);//false
  22. System.out.println(s6 == s7);//false
  23. //intern():判断字符串常量池中是否存在javaEEhadoop值,如果存在,则返回常量池中javaEEhadoop的地址;
  24. //如果字符串常量池中不存在javaEEhadoop,则在常量池中加载一份javaEEhadoop,并返回该对象的地址。
  25. String s8 = s6.intern();
  26. System.out.println(s3 == s8);//true
  27. }

 

 

 



所属网站分类: 技术文章 > 博客

作者:java小王子

链接:http://www.javaheidong.com/blog/article/994/490380ce4f926bb48622/

来源:java黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

8 0
收藏该文
已收藏

评论内容:(最多支持255个字符)