Java的基本问题
JavaGuide
Java基本功
1.Java语言特点:面向对象,跨平台,支持多线程,支持网络编程,健壮性,安全性,编译与解释并存(具有编译型语言和解释型语言的特征)
2.JVM:Java虚拟机,用于运行Java字节码(.class文件),不同的系统使用的是相同的字节码,将.class文件通过JVM解释为机器可以执行的二级制机器码。
3.JDK&&JRE:JDK拥有JRE的一切,还有编译器和工具,能够创建和编译程序。JRE是Java运行时环境。
4.Open JDK和Oracle JDK:Open JDK是一个参考模型且完全开源,Oracle JDK不完全开源但是更稳定,性能方面更好。
5.Java和C++:
- 都是面向对象,支持封装、继承和多态。
- Java不提供指针来直接访问内存,程序内存更加安全。
- Java的类是单继承,C++支持多继承,但是Java的接口可以多继承。
- Java有垃圾回收机制(GC),不需要手动释放内存。
- C语言中,字符串或者字符数组最后都会有一个额外字符‘\0’表示结束。
Java语法
Java常见关键字
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
常用的通配符为: T,E,K,V,?
- ? 表示不确定的 java 类型
- T (type) 表示具体的一个 java 类型
- K V (key value) 分别代表 java 键值中的 Key Value
- E (element) 代表 Element
==和 equals 的区别
-
== 的作用是判断两个对象的地址是不是相等。即判断两个对象是不是同一个对象。(基本数据类型中比较的是值,引用数据类型中比较的是内存地址),其本质比较的都是值,只是引用类型变量存的值是对象的地址。
-
equals()的作用也是判断两个对象是否相等,它不能用于比较基本数据类型的变量。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类。类没有重写equals()方法则是等价于==,类重写了equals()方法则比较的是内容。
-
由于枚举类型确保JVM中仅存在一个常量实例,因此我们可以安全地使用 == 运算符比较两个变量,而且可提供编译时和运行时的安全性。如果两个值均为null 都不会引发 NullPointerException。相反,如果使用equals方法,将抛出 NullPointerException。
为什么重写 equals 时必须重写 hashCode 方法?
- hashCode()介绍:
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在 JDK 的 Object 类中,该方法通常用来将对象的 内存地址 转换为 整数 之后返回。 - 为什么要有 hashCode?
假设当把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals() 方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。 - 为什么重写 equals 时必须重写 hashCode 方法?
如果两个对象相等,则 hashcode 一定也是相同的。两个对象相等,对两个对象分别调用 equals 方法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不一定是相等的 。因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖。 - 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?
因为 hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞(所谓碰撞也就是指的是不同的对象得到相同的 hashCode),也就是说 hashcode 只是用来缩小查找成本。
基本数据类型
《阿里巴巴Java开发手册》:
- 所有整型包装类对象值的比较必须使用equals方法。
- 浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断。
- 【强制】所有的 POJO 类属性必须使用包装数据类型。
- 【强制】RPC 方法的返回值和参数必须使用包装数据类型。
- 【推荐】所有的局部变量使用基本数据类型。
- 正例 : 数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。
- 反例 : 比如显示成交总额涨跌情况,即正负 x%,x 为基本数据类型,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中划线。所以包装数据类型的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。
浮点数之间进行运算会精度丢失,如何解决这个问题呢?
- 一种很常用的方法是:使用使用 BigDecimal 来定义浮点数的值,再进行浮点数的运算操作。
- BigDecimal a = new BigDecimal(“1.0”);
- 使用BigDecimal主要用来操作(大)浮点数,为了防止精度丢失,推荐使用它的 BigDecimal(String) 构造方法来创建对象。
- 可以通过 setScale方法设置保留几位小数以及保留规则。
自动装箱与拆箱
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
包装类和常量池
- Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,Character 创建了数值在[0,127]范围的缓存数据,Boolean 直接返回 True Or False。如果超出对应范围仍然会去创建新的对象。
- 两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。
- 例:
Integer i1=40;Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。
Integer i1 = new Integer(40);这种情况下会创建新的对象。
注:+这个操作符不适用于 Integer 对象,首先Integer 对象进行自动拆箱操作,进行数值相加,且Integer 对象无法与数值进行直接比较,所以自动拆箱转为 int 类型。
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println("i1=i2 " + (i1 == i2));//true
System.out.println("i1=i2+i3 " + (i1 == i2 + i3));//true
System.out.println("i1=i4 " + (i1 == i4));//false
System.out.println("i4=i5 " + (i4 == i5));//false
System.out.println("i4=i5+i6 " + (i4 == i5 + i6));//true
System.out.println("40=i5+i6 " + (40 == i5 + i6));//true
重载和重写的区别
- 重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理,
发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 - 重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法,
重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。
1.返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
2.如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
3.构造方法无法被重写。
4.如果方法的返回类型是void和基本数据类型,则返回值重写时不可修改。
5.如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。
构造器 Constructor 不能被重写,但是可以重载,所以可以看到一个类中有多个构造函数的情况。
深拷贝 vs 浅拷贝
- 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
- 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
面向对象和面向过程的区别
- 面向过程 :面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较消耗资源。
- 面向对象 :面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
Java 中定义一个不做事且没有参数的构造方法的作用
- Java 程序在执行子类的构造方法之前,如果没有用 super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super()来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。
成员变量与局部变量的区别有哪些?
- 从语法形式上看:成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
- 从变量在内存中的存储方式来看:如果成员变量是使用static修饰的,那么这个成员变量是属于类的,如果没有使用static修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
- 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
- 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
对象实体与对象引用有何不同?
- new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向 0 个或 1 个对象,一个对象可以有 n 个引用指向它。
一个类的构造方法的作用是什么?
- 主要作用是完成对类对象的初始化工作。
构造方法有哪些特性?
- 名字与类名相同。
- 没有返回值,但不能用 void 声明构造函数。
- 生成类的对象时自动执行,无需调用。
在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?
- 帮助子类做初始化工作。
面向对象三大特征
- 封装:是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。
- 继承:是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。可以快速地创建新的类,提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高开发效率。
- 多态:具体表现为父类的引用指向子类的实例。
静态方法和实例方法有何不同
- 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。
线程安全性
- String 类中使用 final 关键字修饰字符数组来保存字符串,所以String 对象是不可变的。也就可以理解为常量,线程安全。
- 而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。而StringBuffer 对父类的公共方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
- 每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
- 操作少量的数据: 适用 String
- 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
Object 类的常见方法总结
public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。
public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。
protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。
public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。
public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。
public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。
public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作
Java 序列化中如果有些字段不想进行序列化,怎么办?
- 使用 transient 关键字修饰。
- transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
- transient 只能修饰变量,不能修饰类和方法。
获取用键盘输入常用的两种方法
- 方法 1:通过 Scanner
Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close();
- 方法 2:通过 BufferedReader
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();
反射机制
- JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。
- 优点: 运行期类型的判断,动态加载类,提高代码灵活度。
- 缺点: 1,性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 java 代码要慢很多。2,安全问题,让我们可以动态操作改变类的属性同时也增加了类的安全隐患。
- 应用:使用 JDBC 连接数据库时使用 Class.forName()通过反射加载数据库的驱动程序;pring 框架的 IOC(动态加载管理 Bean)创建对象以及 AOP(动态代理)功能都和反射有联系;动态配置实例的属性;等
获取 Class 对象的四种方式
- 知道具体类的情况下可以使用:
Class alunbarClass = TargetObject.class; - 通过 Class.forName()传入类的路径获取:
Class alunbarClass1 = Class.forName(“cn.javaguide.TargetObject”); - 通过对象实例instance.getClass()获取:
Employee e = new Employee();
Class alunbarClass2 = e.getClass(); - 通过类加载器xxxClassLoader.loadClass()传入类路径获取:
class clazz = ClassLoader.LoadClass(“cn.javaguide.TargetObject”);
异常和错误
- 在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类 Exception(异常)和 Error(错误)。Exception 又可以分为 受检查异常(必须处理) 和 不受检查异常(可以不处理)。
- 除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。
- try块用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。finally 语句一直会执行,当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。
- Java中资源需要我们调用close()方法来手动关闭时,一般情况下我们都是通过try-catch-finally语句来实现这个需求,但是使用 try-with-resources 实现起来更简单,可以自动关闭一个或多个资源,通过使用分号分隔,可以在try-with-resources块中声明多个资源。
3 种特殊情况下,finally 块不会被执行:
- 在 try 或 finally 块中用了 System.exit(int)退出程序。但是,如果 System.exit(int) 在异常语句之后,finally 还是会被执行。
- 程序所在的线程死亡。
- 关闭 CPU。
Throwable 类常用方法
- public string getMessage():返回异常发生时的简要描述
- public string toString():返回异常发生时的详细信息
- public string getLocalizedMessage():返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
- public void printStackTrace():在控制台上打印 Throwable 对象封装的异常信息
线程的基本状态
- Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态
- 线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 RUNNING(运行) 状态。当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()方法之后将会进入到 TERMINATED(终止) 状态。
Java 中 IO 流分为几种?
- 按照流的流向分,可以分为输入流和输出流;
- 按照操作单元划分,可以划分为字节流和字符流;
- 按照流的角色划分为节点流和处理流。
- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
- 字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
BIO,NIO,AIO 有什么区别?
- BIO (Blocking I/O): 同步阻塞 I/O 模型,数据的读取写入必须阻塞在一个线程内等待其完成,适用于低负载、低并发的应用程序。
- NIO (Non-blocking/New I/O): 同步非阻塞的 I/O 模型,对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
- AIO (Asynchronous I/O):异步非阻塞的 IO 模型,异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
Arrays.asList()使用指南
- 传入的对象必须是对象数组。可以使用它将一个数组转换为一个List集合,将数组转换为集合后,底层其实还是数组,不能使用其修改集合相关的方法,它的add/remove/clear会抛出异常。
如何正确的将数组转换为ArrayList?
- 最简便的方法(推荐)
List list = new ArrayList<>(Arrays.asList(“a”, “b”, “c”)) - 使用 Java8 的Stream(推荐)
Integer [] myArray = { 1, 2, 3 };
List myList = Arrays.stream(myArray).collect(Collectors.toList());
//基本类型也可以实现转换(依赖boxed的装箱操作)
int [] myArray2 = { 1, 2, 3 };
List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
不要在 foreach 循环里进行元素的 remove/add 操作
- 如果要进行remove操作,可以调用迭代器的 remove 方法而不是集合类的 remove 方法。因为如果列表在任何时间从结构上修改创建迭代器之后,以任何方式除非通过迭代器自身remove/add方法,迭代器都将抛出一个ConcurrentModificationException,这就是单线程状态下产生的 fail-fast 机制。
- Java8开始,可以使用removeIf()方法删除满足特定条件的元素。
this 和 super
- this用于访问类的当前实例的变量和方法。
- super关键字用于从子类访问父类的变量和方法。
- 在构造器中使用 super() 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。
- this、super不能用在static方法中。(被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享。
本文地址:https://blog.csdn.net/J_i_ang_/article/details/113923024