Java基础

Java基础

面向对象和面向过程的区别

  • 面向过程:面向过程的性能比面向对象高。因为类调用时需要实例化,开销比较大,比较消耗资源,所以当资源是最重要的考量因素时,比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发。但是,面向过程没有面向对象易维护、易复用、易扩展
  • 面向对象:面向对象易维护、易复用、易扩展。因为面向对象有封装、继承、多态性的特点。所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是面向对象性能比面向过程低。

这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java性能差的主要原因并不是它是面向对象的语言,而是Java使半编译的语言,最终执行代码并不是可以直接被cpu执行的二进制机械码。

而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其他一些面向过程的脚本语言性能也不一定比Java好。

Java语言有哪些特点?

1.简单易学

2.面向对象(封装、继承、多态)

3.平台无关性(Java虚拟机实现平台无关性)

4.可靠性

5.安全性

6.支持多线程

7.支持网络编程并且很方便

8.编译与解释并存

关于JVM、JDK、和JRE的解释

Java虚拟机(JVM)是运行Java字节码的虚拟机。JVM有针对不同系统的特定实现,目的是使用相同的字节码,它们会给出相同的结果。

什么是字节码?采用字节码的好处是什么?
在Java中,JVM可以理解的代码就叫做字节码(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机。Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,并且,由于字节码并不针对一种特定的机器,因此,Java程序无序重新编译便可在多种不同的操作系统的计算机上运行

Java程序从源代码到运行一般有下面的三步:

Java程序运行过程

我们需要格外注意的是.class->机器码这一步。在这一步JVM类加载器首先加载字节码文件,然后通过解释器逐行解释,这种方式的执行速度会相对比较慢。而且,有些返回方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了JIT编译器而JIT属于运行时编译。当JIT编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定高于Java解释器的。这也解释了我们为什么经常说Java是编译与解释并存的语言。

HotSpot采用了惰性评估的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是JIT需要编译的部分。JVM会根据每次被执行的情况收集信息并相应的做出一些优化,因此执行的次数越多,它的速度就越快。JDK9引入了一种新的编译模式AOT,它是直接将字节码编译成机器码,这样就避免了JIT预热各方面的开销。JDK支持分层编译和AOT协作使用。但是,AOT编译器的编译质量是肯定比不上JIT编译器的。

总结
Java虚拟机(JVM)是运行Java字节码的虚拟机。JVM有针对不同系统的特定实现,目的是使用相同的字节码,它们会给出相同的结果。字节码和不同系统的JVM实现是Java语言“一次编译,随处可以运行”的关键。

JDK和JRE

JDK是Java Development Kit,它是功能齐全的Java SDK。它拥有JRE所拥有的一切,还有编译器(Javac)和工具(如javadoc和jdb)。它能够创建和编译程序。

JRE是Java运行时环境,它是运行已编译Java程序所需的所有内容的集合,包括Java虚拟机(JVM),Java类库,java命令和其他一些接触的构件。但是它不能用于创建新程序。

如果你只是为了运行一下Java程序的话,那么你只需要安装JRE就可以了。如果你需要进一步进行一些Java编程方面的工作,那么你就需要安装JDK了。但是,这不是绝对的。有时,即使你不打算在计算机上进行任何Java开发,仍然需要安装JDK。例如,如果要使用JSP部署Web应用程序,那么从技术上将,你只是在应用程序服务中运行Java程序。那你为什么需要JDK呢?因为应用程序服务器会将JSP转换为Java Servlet,并且需要使用JDK来编译Servlet。

Oracle JDK和OpenJDK的对比

对于Java7,没什么关键的地方。OpenJDK项目主要基于Sun捐赠的HotSpot源代码。此外,OpenJDK被选为Java7参考实现,由Oracle工程师维护。关于JVM,JDK,JRE和OpenJDK之间的区别,Orcle博客帖子的答案如下:

问:OpenJDK存储库中的源代码与用于构建Oracle JDK的代码之间的区别?

答:非常接近。我们的Oracle JDK版本构建过程基于OpenJDK 7构建,只添加了几个部分,例如部署代码,其中包括Oracle的Java插件和Java WebStart的实现,以及一些封闭的源代码派对组件,如图形光栅化器,一些开源的第三方组件,如Rhino,以及一些零碎的东西,如附加文档或第三方字体。展望未来,我们的目的是开源Oracle JDK的所有部分,除了我们考虑的商业功能部分。

总结:
1.Oracle JDK大概每6个月发一次主要版本,而Open JDK版本大概每三个月发布一次。但这不是固定的。

2.OpenJDK是一个参考模型并且是完全开源的,而Oracle JDK是OpenJDK的一个实现,并不是完全开源的

3.Oracle JDK比OpenJDK更稳定。OpenJDK和Oracle JDK的代码几乎相同,但Oracle JDK有更多的类和一些错误修复。因此,如果想要开发企业/商业啊软件,建议选择Oracle JDK,因为它经过了彻底的测试。

4.在响应性和JVM性能方面,Oracle JDK与OpenJDK相比提供了更好的性能

5.Oracle JDK不会为即将发布的版本提供长期的支持,用户每次都必须通过更新到最新的版本获得支持。

6.Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPLv2许可获得许可

Java和C++的区别?

  • 都是面向对象的语言,都支持封装、继承、多态
  • Java不提供指针来直接访问内存,程序内存更加安全
  • Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是接口可以多继承
  • Java有自动内存管理机制,不需要程序员手动释放无用的内存
  • 在C语言中,字符串或字符数组最后都会有一个额外的字符’\0’来表示结束。但是,Java语言中没有结束符这一概念。

Java中无需空字符的原因

Java里面一切都是对象,是对象的话,字符串肯定就有长度,既然有长度,编译器就可以确定要输出的字符的个数,当然也就没有必要去浪费那1字节的空间用以标明字符串的结束了。

字符串常量和字符型常量的区别?

1.形式上:字符常量是单引号引起的一个字符;字符串常量是双引号引起的若干个字符

2.含义上:字符常量相当于一个整型值(ASCII值),可以参加表达式运算;字符串常量代表一个地址值(该字符串在内存中存放的位置0

3.占内存的大小:字符常量只占2个字节;字符串常量占若干个字节(char在Java中占两个字节)

构造器Constructor是否可被override?

Constructor不能被override(重写),但是可以overload(重载),所以可以看到一个类中有多个构造函数的情况

重载和重写的区别

重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理

重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法

重载:发生在同一个类中,方法名必须相同,参数类型不同,个数不同,顺序不同,方法返回值和访问修饰符可以相同

综上:重载就是同一个类中多个同名的方法根据不同的传参来执行不同的逻辑处理

重写:

重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。

1.返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类

2.如果父类方法的访问修饰符为private/final/static,则子类就不能重写该方法,但是被static修饰的方法能够被再次声明

3.构造方法无法被重写

综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变

区别点重载方法重写方法
发生范围同一个类子类
参数列表必须修改一定不能修改
返回类型可修改子类方法返回值类型应比父类方法返回值类型更小或相等
异常可修改子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
访问修饰符可修改一定不能做更严格的限制(可以降低限制)
发生阶段编译期运行期

方法的重写要遵顼“两同两小一大”:

  • “两同”即方法名、参数列表相同
  • “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类型应比父类方法声明抛出的异常类更小或者相等
  • "一大"指的是子类方法的访问权限应比父类方法的访问权限更大或相等

Java面向对象编程的三大特性:封装 继承 多态

封装

封装把一个对象的属性私有化,同时提供一些可以被外界访问的属的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。

继承

继承是使用已存在的类的定义作为基础创建新类的技术,新类的定义可以增加新的数据或者新的功能,也可以使用父类的功能,但不能选择性的继承父类。通过使用继承我们能够非常方便地复用以前的代码。

关于继承如下三点需要记住:

1.子类拥有父类对象的所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问的,只是拥有

2.子类可以拥有自己的属性和方法,即子类可以对父类进行扩展

3.子类可以用自己的方式实现父类的方法

多态

所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。

在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)

String StringBuffer StringBuilder的区别是什么?String为什么是不可变的?

可变性
简单的来说:String类中使用final关键字修饰字符数组来保存字符串, private final byte value[],所以String对象是不可变的。

而StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,但是没有使用final关键字修饰,所以这两种对象都是可变的。

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
     */
    int count;

    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

线程安全性
String中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder是StringBuilder和StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append]insert\indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以线程是安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。

性能
每次对String类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String对象。StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StringBuilder相比使用StringBuffer仅能获得10%~15%左右的性能提升,但却要冒多线程不安全的风险。

对三者使用的总结:

1.操作少量的数据:使用String

2.单线程操作字符串缓冲区下操作大量数:使用StringBuilder

3.多线程下操作字符串缓冲区次啊操作大量数据:使用StringBuffer

自动装箱和拆箱

装箱:将基本类型用它们对应的引用类型包装起来

拆箱:将包装类型转换为基本数据类型

Java SE5之前,如果要生成一个数值为10的Integer对象,必须这样进行:

Integer i = new Integer(10);

而在Java SE5开始就提供了自动装箱的特性,如果要生成一个Integer对象,只需要:

Integer i = 10;
Integer i = 10; //装箱
int n = i;  ///拆箱

面试题

public class Main {
    public static void main(String[] args) {
         
        Integer i1 = 100;
        Integer i2 = 100;
        Integer i3 = 200;
        Integer i4 = 200;
         
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}

输出的结果为:

true
false

输出结果表明:i1和i指向的是同一个对象,而i3和i4指向的是不同的对象。

原因在于,在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。

public class Main {
    public static void main(String[] args) {
         
        Double i1 = 100.0;
        Double i2 = 100.0;
        Double i3 = 200.0;
        Double i4 = 200.0;
         
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}

输出结果:

false
false

在某个范围内的整型数值是有限的,而浮点数却不是。

public class Main {
    public static void main(String[] args) {
         
        Boolean i1 = false;
        Boolean i2 = false;
        Boolean i3 = true;
        Boolean i4 = true;
         
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}

输出结果:

true
true

Integer i = new Integer(xxx)和Integer i = xxx这两种方式的区别:

  • 第一种方式不会触发自动装箱的过程;而第二种方式会触发
  • 在执行效率和资源占用上的区别。第二种方式的执行效率和资源占用一般性情况下要优于第一种情况。
public class Main {
    public static void main(String[] args) {
         
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;
        Integer d = 3;
        Integer e = 321;
        Integer f = 321;
        Long g = 3L;
        Long h = 2L;
         
        System.out.println(c==d);
        System.out.println(e==f);
        System.out.println(c==(a+b));
        System.out.println(c.equals(a+b));
        System.out.println(g==(a+b));
        System.out.println(g.equals(a+b));
        System.out.println(g.equals(a+h));
    }
}

输出结果:

true
false
true
true
true
false
true

当"=="运算符的两个操作数都是包装器的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式,则比较的是数值(即会触发自动拆箱的过程)另外,对于包装器类型,equals方法并不会进行类型转换

在一个静态方法内调用一个非静态方法为什么是非法的?

由于静态方法可以不通过对象直接调用,因此在静态方法中,不能调用其他非静态变量,也不可以访问非静态变量成员

在Java中定义一个不做事且没有参数的构造方法的作用

Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中有没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决方法是在父类中加上一个不做事且没有参数的构造方法。

接口和抽象类的区别是什么?

1.接口的方法默认是public,所有方法在接口中不能有实现(Java8开始接口方法可以有默认实现),而抽象类可以有非抽象的方法

2,接口中除了staticfinal变量,不能有其他变量,而在抽象类中则不一定

3.一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过extends关键字拓展多个接口

4.接口方法默认修饰符是public,抽象方法可以有publicprotecteddefault这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰)

5.从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。

1.在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。

2.JDK9的接口被允许定义私有方法

JDK7~JDK9 Java中接口概念的变化

1.在JDK7或更早的版本中,接口里面只能由常量变量和抽象方法这些接口方法必须由选择实现接口的类实现

2.JDK8的时候接口可以有默认方法和静态方法功能

3.JDK9在接口中引入了私有方法和私有静态方法

成员变量和局部变量的区别有哪些?

1.从语法形式上来看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被publicprivatestatic等修饰符修饰,而局部变量不能被访问控制修饰符及static所修饰;但是成员变量和局部变量都可以被final修饰

2.从变量在内存中的存储方式来看:如果成员变量是使用static修饰的,那么这个成员变量是属于类的,如果没有使用static修饰,这个成员变量是属于实例的。对象存于堆内存,如果局部变量类型为基本数据类型,那么存储在栈内存,如果为引用数据类型,那存放的是指向堆内存对象的引用或者是指向常量池中的地址

3.从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失

4.成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被final修饰的成员变量也必须被显示地赋值),而局部变量不会自动赋值

创建一个对象用什么运算符?对象实体与对象引用有何不同?

new运算符,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条身子系住一个气球)

什么是方法的返回值?返回值在类的方法里的作用是什么?

方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用:接收出结果,使得它可以用于其他的操作。

一个类的构造方法的作用是什么?若一个类没有声明构造方法,该程序能正确执行吗?为什么?

主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。

构造方法有哪些特性?

1.名字和类名相同

2.没有返回值,但不能用void声明构造函数

3.生成类的对象时自动执行,无需调用

静态方法和实例方法有何不同?

1.在外部调用静态方法时,可以使用“类名.方法名”的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。

2静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法无此限制。

对象的相等与指向它们的引用相等,两者有什么不同?

对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是它们指向的内存地址是否相等。

在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?

帮助子类做初始化工作。

==与equals(重要)

:它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型比较的是值,引用数据类型比较的是内存地址)

equals():它的作用是比较两个对象是否相等。但它一般有两种使用情况:

  • 情况一:类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过”=="比较这两个对象
  • 情况二:类覆盖了equals()方法。一般,我们都覆盖equals()方法来比较两个对象的内容是否相等;若它们的内容相等,则返回true。

hashCode与equals(重要)

1)hashCode()介绍:

hashCode()的作用是获取哈希码,也称为散列表;它实际上是返回一个Int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在JDK的Object类中,这就意味着Java中的任何类都包含有hashCode()函数。另外需要注意的是:Object的hashCode()方法是本地方法,也就是用c语言或c++实现的,该方法通常用来将对象的内存地址转换为整数之后返回。

2)为什么要有hashCode()?

我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode?

当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals() 方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

3)为什么重写equals时必须重写hashCode方法?

如果两个对象相等,则hashCode一定也是相同的。两个对象相等,对两个对象分别调用equals方法都返回true。但是,两个对象有相同的hashCode值,它们也不一定是相等的。因此,equals方法被覆盖过,则hashCode方法也必须被覆盖。

hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

没有覆盖equals()方法的情况

import java.util.*;
import java.lang.Comparable;

/**
 * @desc equals()的测试程序。
 *
 * @author skywang
 * @emai kuiwu-wang@163.com
 */
public class EqualsTest1{

    public static void main(String[] args) {
        // 新建2个相同内容的Person对象,
        // 再用equals比较它们是否相等
        Person p1 = new Person("eee", 100);
        Person p2 = new Person("eee", 100);
        System.out.printf("%s\n", p1.equals(p2));
    }

    /**
     * @desc Person类。
     */
    private static class Person {
        int age;
        String name;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String toString() {
            return name + " - " +age;
        }
    }
}

运行结果:

false

结果分析

​ 我们通过 p1.equals(p2) 来“比较p1和p2是否相等时”。实际上,调用的Object.java的equals()方法,即调用的 (p1==p2) 。它是比较“p1和p2是否是同一个对象”。
​ 而由 p1 和 p2 的定义可知,它们虽然内容相同;但它们是两个不同的对象!因此,返回结果是false。

覆盖equals()方法的情况

import java.util.*;
import java.lang.Comparable;

/**
 * @desc equals()的测试程序。
 *
 * @author skywang
 * @emai kuiwu-wang@163.com
 */
public class EqualsTest2{

    public static void main(String[] args) {
        // 新建2个相同内容的Person对象,
        // 再用equals比较它们是否相等
        Person p1 = new Person("eee", 100);
        Person p2 = new Person("eee", 100);
        System.out.printf("%s\n", p1.equals(p2));
    }

    /**
     * @desc Person类。
     */
    private static class Person {
        int age;
        String name;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String toString() {
            return name + " - " +age;
        }

        /**
         * @desc 覆盖equals方法
         */
        @Override
        public boolean equals(Object obj){
            if(obj == null){
                return false;
            }

            //如果是同一个对象返回true,反之返回false
            if(this == obj){
                return true;
            }

            //判断是否类型相同
            if(this.getClass() != obj.getClass()){
                return false;
            }

            Person person = (Person)obj;
            return name.equals(person.name) && age==person.age;
        }
    }
}

运行结果:

true

结果分析

我们在EqualsTest2.java 中重写了Person的equals()函数:当两个Person对象的 name 和 age 都相等,则返回true。因此,运行结果返回true。

讲到这里,顺便说一下java对equals()的要求。有以下几点:

1. 对称性:如果x.equals(y)返回是"true",那么y.equals(x)也应该返回是"true"。
2. 反射性:x.equals(x)必须返回是"true"。
3. 类推性:如果x.equals(y)返回是"true",而且y.equals(z)返回是"true",那么z.equals(x)也应该返回是"true"。
4. 一致性:如果x.equals(y)返回是"true",只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是"true"。
5. 非空性,x.equals(null),永远返回是"false";x.equals(和x不同类型的对象)永远返回是"false"。

会创建“类对应的散列表”

​ 这里所说的“会创建类对应的散列表”是说:我们会在HashSet, Hashtable, HashMap等等这些本质是散列表的数据结构中,用到该类。例如,会创建该类的HashSet集合。

​ 在这种情况下,该类的“hashCode() 和 equals() ”是有关系的:
​ 1)如果两个对象相等,那么它们的hashCode()值一定相同。
​ 这里的相等是指,通过equals()比较两个对象时返回true。
​ 2)如果两个对象hashCode()相等,它们并不一定相等。
​ 因为在散列表中,hashCode()相等,即两个键值对的哈希值相等。然而哈希值相等,并不一定能得出键值对相等。补充说一句:“两个不同的键值对,哈希值相等”,这就是哈希冲突。

​ 此外,在这种情况下。若要判断两个对象是否相等,除了要覆盖equals()之外,也要覆盖hashCode()函数。否则,equals()无效。
例如,创建Person类的HashSet集合,必须同时覆盖Person类的equals() 和 hashCode()方法。
​ 如果单单只是覆盖equals()方法。我们会发现,equals()方法没有达到我们想要的效果。

import java.util.*;
import java.lang.Comparable;

/**
 * @desc 比较equals() 返回true 以及 返回false时, hashCode()的值。
 *
 * @author skywang
 * @emai kuiwu-wang@163.com
 */
public class ConflictHashCodeTest1{

    public static void main(String[] args) {
        // 新建Person对象,
        Person p1 = new Person("eee", 100);
        Person p2 = new Person("eee", 100);
        Person p3 = new Person("aaa", 200);

        // 新建HashSet对象
        HashSet set = new HashSet();
        set.add(p1);
        set.add(p2);
        set.add(p3);

        // 比较p1 和 p2, 并打印它们的hashCode()
        System.out.printf("p1.equals(p2) : %s; p1(%d) p2(%d)\n", p1.equals(p2), p1.hashCode(), p2.hashCode());
        // 打印set
        System.out.printf("set:%s\n", set);
    }

    /**
     * @desc Person类。
     */
    private static class Person {
        int age;
        String name;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String toString() {
            return "("+name + ", " +age+")";
        }

        /**
         * @desc 覆盖equals方法
         */
        @Override
        public boolean equals(Object obj){
            if(obj == null){
                return false;
            }

            //如果是同一个对象返回true,反之返回false
            if(this == obj){
                return true;
            }

            //判断是否类型相同
            if(this.getClass() != obj.getClass()){
                return false;
            }

            Person person = (Person)obj;
            return name.equals(person.name) && age==person.age;
        }
    }
}

运行结果:

p1.equals(p2) : true; p1(1169863946) p2(1690552137)
set:[(eee, 100), (eee, 100), (aaa, 200)]

结果分析

​ 我们重写了Person的equals()。但是,很奇怪的发现:HashSet中仍然有重复元素:p1 和 p2。为什么会出现这种情况呢?

​ 这是因为虽然p1 和 p2的内容相等,但是它们的hashCode()不等;所以,HashSet在添加p1和p2的时候,认为它们不相等。

import java.util.*;
import java.lang.Comparable;

/**
 * @desc 比较equals() 返回true 以及 返回false时, hashCode()的值。
 *
 * @author skywang
 * @emai kuiwu-wang@163.com
 */
public class ConflictHashCodeTest2{

    public static void main(String[] args) {
        // 新建Person对象,
        Person p1 = new Person("eee", 100);
        Person p2 = new Person("eee", 100);
        Person p3 = new Person("aaa", 200);
        Person p4 = new Person("EEE", 100);

        // 新建HashSet对象
        HashSet set = new HashSet();
        set.add(p1);
        set.add(p2);
        set.add(p3);

        // 比较p1 和 p2, 并打印它们的hashCode()
        System.out.printf("p1.equals(p2) : %s; p1(%d) p2(%d)\n", p1.equals(p2), p1.hashCode(), p2.hashCode());
        // 比较p1 和 p4, 并打印它们的hashCode()
        System.out.printf("p1.equals(p4) : %s; p1(%d) p4(%d)\n", p1.equals(p4), p1.hashCode(), p4.hashCode());
        // 打印set
        System.out.printf("set:%s\n", set);
    }

    /**
     * @desc Person类。
     */
    private static class Person {
        int age;
        String name;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String toString() {
            return name + " - " +age;
        }

        /**
         * @desc重写hashCode
         */
        @Override
        public int hashCode(){
            int nameHash =  name.toUpperCase().hashCode();
            return nameHash ^ age;
        }

        /**
         * @desc 覆盖equals方法
         */
        @Override
        public boolean equals(Object obj){
            if(obj == null){
                return false;
            }

            //如果是同一个对象返回true,反之返回false
            if(this == obj){
                return true;
            }

            //判断是否类型相同
            if(this.getClass() != obj.getClass()){
                return false;
            }

            Person person = (Person)obj;
            return name.equals(person.name) && age==person.age;
        }
    }
}

运行结果:

p1.equals(p2) : true; p1(68545) p2(68545)
p1.equals(p4) : false; p1(68545) p4(68545)
set:[aaa - 200, eee - 100]

结果分析

​ 这下,equals()生效了,HashSet中没有重复元素。
比较p1和p2,我们发现:它们的hashCode()相等,通过equals()比较它们也返回true。所以,p1和p2被视为相等。
比较p1和p4,我们发现:虽然它们的hashCode()相等;但是,通过equals()比较它们返回false。所以,p1和p4被视为不相等。

为什么Java中只有值传递?

首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。 它用来描述各种程序设计语言(不只是 Java)中方法参数传递方式。

Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。

方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。

很多程序设计语言(特别是,C++和 Pascal)提供了两种参数传递的方式:值调用和引用调用。有些程序员(甚至本书的作者)认为 Java 程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。由于这种误解具有一定的普遍性,所以下面给出一个反例来详细地阐述一下这个问题。

public class Test {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Student s1 = new Student("小张");
        Student s2 = new Student("小李");
        Test.swap(s1, s2);
        System.out.println("s1:" + s1.getName());
        System.out.println("s2:" + s2.getName());
    }

    public static void swap(Student x, Student y) {
        Student temp = x;
        x = y;
        y = temp;
        System.out.println("x:" + x.getName());
        System.out.println("y:" + y.getName());
    }
}

运行结果:

x:小李
y:小张
s1:小张
s2:小李

方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝

简述线程、程序、进程的基本概念。以及它们之间得关系是什么?

线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。

线程有哪些基本状态?

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态

Java线程的状态

Java线程状态变迁

关于final关键字的一些总结

final 关键字主要用在三个地方:变量、方法、类。

  1. 对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
  2. 当用 final 修饰一个类时,表明这个类不能被继承。final 类中的所有成员方法都会被隐式地指定为 final 方法。
  3. 使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。类中所有的 private 方法都隐式地指定为 final。

Java中的异常处理

Java异常类层次结构图

img

img

在 Java中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类 Exception(异常)和 Error(错误)。Exception 能被程序本身处理(try-catch), Error 是无法处理的(只能尽量避免)。

ExceptionError 二者都是 Java 异常处理的重要子类,各自都包含大量子类。

  • Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 受检查异常(必须处理) 和 不受检查异常(可以不处理)。
  • ErrorError 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获 。例如,Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
受检查异常

Java 代码在编译过程中,如果受检查异常没有被 catch/throw 处理的话,就没办法通过编译 。比如下面这段 IO 操作的代码。

check-exception

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于检查异常 。常见的受检查异常有: IO 相关的异常、ClassNotFoundExceptionSQLException…。

不受检查异常

Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。

RuntimeException 及其子类都统称为非受检查异常,例如:NullPointExecrptionNumberFormatException(字符串转换为数字)、ArrayIndexOutOfBoundsException(数组越界)、ClassCastException(类型转换错误)、ArithmeticException(算术错误)等。

Throwable类常用方法
  • public string getMessage():返回异常发生时的简要描述
  • public string toString():返回异常发生时的详细信息
  • public string getLocalizedMessage():返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
  • public void printStackTrace():在控制台上打印 Throwable 对象封装的异常信息
异常处理总结
  • try块: 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • catch块: 用于处理 try 捕获到的异常。
  • finally 块: 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。

在以下 4 种特殊情况下,finally 块不会被执行:

  1. finally 语句块第一行发生了异常。 因为在其他行,finally 块还是会得到执行
  2. 在前面的代码中用了 System.exit(int)已退出程序。 exit 是带参函数 ;若该语句在异常语句之后,finally 会执行
  3. 程序所在的线程死亡。
  4. 关闭 CPU。

注意: 当 try 语句和 finally 语句中都有 return 语句时,在方法返回之前,finally 语句的内容将被执行,并且 finally 语句的返回值将会覆盖原始的返回值。如下:

    public static int f(int value) {
        try {
            return value * value;
        } finally {
            if (value == 2) {
                return 0;
            }
        }
    }

如果调用 f(2),返回值将是 0,因为 finally 语句的返回值覆盖了 try 语句块的返回值。

Java序列化中如果有些字段不想进行序列化,怎么办?

对于不想进行序列化的变量,使用 transient 关键字修饰。

transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。

获取用键盘输入常用的两种方法

方法一:通过Scanner

Scanner input = new Scanner(System.in);
String s  = input.nextLine();
input.close();

方法二:通过BufferedReader

BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();

Java中的IO流

Java中的IO流分为几种?
  • 按照流的流向分,可以分为输入流和输出流
  • 按照操作单元分,可以划分为字节流和字符流
  • 按照流的角色划分为节点流和处理流

JavaIO流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。

  • InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
  • OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

按操作方式分类结构图:

IO-操作方式分类

按操作对象分类:

IO-操作对象分类

既然有了字节流,为什么还要有字符流

问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?

回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。

BIO,NIO,AIO有什么区别?

  • BIO (Blocking I/O): 同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机 1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
  • NIO (Non-blocking/New I/O): NIO 是一种同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 NIO 提供了与传统 BIO 模型中的 SocketServerSocket 相对应的 SocketChannelServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
  • AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步 IO 的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO 操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。

深拷贝和浅拷贝

  1. 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
  2. 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

deep and shallow copy

参考:

  • https://stackoverflow.com/questions/1906445/what-is-the-difference-between-jdk-and-jre
  • https://www.educba.com/oracle-vs-openjdk/
    tor,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 NIO 提供了与传统 BIO 模型中的 SocketServerSocket 相对应的 SocketChannelServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
  • AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步 IO 的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO 操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。

深拷贝和浅拷贝

  1. 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
  2. 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

[外链图片转存中…(img-KLGSMLuW-1610878615383)]

参考:

  • https://stackoverflow.com/questions/1906445/what-is-the-difference-between-jdk-and-jre
  • https://www.educba.com/oracle-vs-openjdk/
  • https://stackoverflow.com/questions/22358071/differences-between-oracle-jdk-and-openjdk?answertab=active#tab-top
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值