JAVA问题
JDK 和 JRE 有什么区别?
https://blog.csdn.net/singit/article/details/62040688
https://www.cnblogs.com/lkboy/p/4159379.html
JDK包含了JRE,同时还包含了编译java源码的编译器javac,还包含了很多java程序调试和分析的工具:jconsole,jvisualvm等工具软件,还包含了java程序编写所需的文档和demo例子程序。
如果你需要运行java程序,只需安装JRE就可以了。如果你需要编写java程序,需要安装JDK。
一. JDK——开发环境
https://zhuanlan.zhihu.com/p/46562903 JDK
JDK是Sun公司(已被Oracle收购)针对Java开发员的软件开发工具包。是提供给程序员使用的。
JDK主要包含三部分,
第一部分就是Java运行时环境,JVM。
第二部分就是Java的基础类库,这个类库的数量还是非常可观的。
第三部分就是Java的开发工具,它们都是辅助你更好的使用Java的利器。
看一下JDK的安装目录。在目录下面有六个文件夹、一个src类库源码压缩包、和其他几个声明文件。
其中,真正在运行java时起作用的是以下四个文件夹:bin、include、lib、 jre。
现在我们可以看出这样一个关系,JDK包含JRE,而JRE包含JVM。
bin:最主要的是编译器(javac.exe)
include:java和JVM交互用的头文件
lib:类库
jre:java运行环境(注意:这里的bin、lib文件夹和jre里的bin、lib是不同的)
总的来说JDK是用于java程序的开发,而jre则是只能运行class而没有编译的功能。
二. JRE——运行环境
JRE是java运行时环境,包含了java虚拟机,java基础类库。是使用java语言编写的程序运行所需要的软件环境,是提供给想运行java程序的用户使用的。
JRE根据不同操作系统(如:windows,linux等)和不同JRE提供商(IBM,ORACLE等)有很多版本,最常用的是Oracle公司收购SUN公司的JRE版本。
-
JDK中的JRE
文件夹中jdk
中包含的文件夹jre
,在jre
的bin
目录里有个jvm.dll
,JRE是运行环境,运行在JVM虚拟机上,jre
的lib
目录中放的是一些JAVA类库的class文件,已经打包成jar文件。 -
独立的JRE
单独的JRE里自然也是有JVM的。
光有JVM还不能成class的执行,因为在解释class的时候JVM需要调用解释所需要的类库lib。
在JDK的安装目录里你可以找到jre目录,里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib和起来就称为jre。
所以,在你写完java程序编译成.class之后,你可以把这个.class文件和jre一起打包发给朋友,这样你的朋友就可以运行你写程序了。
三. JVM——转换环境
JVM就是java虚拟机的缩写。
它是整个java实现跨平台的最核心的部分,所有的 java 程序会首先被编译为.class
的类文件,这种类文件可以在虚拟机上执行,也就是说class并不直接与机器的操作系统相对应,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行。
四. javac
javac是java语言编程编译器。全称java compiler. javac工具读有java语言编写的类和接口的定义,并将它们编译成字节代码的class文件。
javac
编译.java
文件成为.class
文件
为什么Sun要让JDK安装两套相同的JRE?
这是因为JDK里面有很多用Java所编写的开发工具(如javac.exe、jar.exe等),而且都放置在 \lib\tools.jar 里。
从下面例子可以看出,先将tools.jar改名为tools1.jar,然后运行javac.exe,显示如下结果: Exception in thread “main” java.lang.NoClassDefFoundError: com/sun/tools/javac /Main 这个意思是说,你输入javac.exe与输入 java -cp c:\jdk\lib\tools.jar com.sun.tools.javac.Main 是一样的,会得到相同的结果。
从这里我们可以证明javac.exe只是一个包装器(Wrapper),而制作的目的是为了让开发者免于输入太长的指命。而且可以发现\lib目录下的程序都很小,不大于2 9K,从这里我们可以得出一个结论。
就是JDK里的工具几乎是用Java所编写,所以也是Java应用程序,因此要使用JDK所附的工具来开发Java程序,也必须要自行附一套JRE才行,所以位于C:\Program Files\Java目录下的那套JRE就是用来运行一般Java程序用的。
如果一台电脑安装两套以上的JRE,谁来决定呢?
这个重大任务就落在java.exe身上。Java.exe的工作就是找到合适的JRE来运行Java程序。
Java.exe依照底下的顺序来查找JRE:自己的目录下有没有JRE;父目录有没有JRE;查询注册表: [HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment] 所以java.exe的运行结果与你的电脑里面哪个JRE被执行有很大的关系。
== 和 equals 的区别是什么?
一. ==
比较的是值是否相等
- 如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等
- 如果作用于引用类型的变量,则比较的是所指向的对象的地址
二. equals
比较的是是否是同一个对象
equals方法不能作用于基本数据类型的变量
- 如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址
- 诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容
equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,所以说所有类中的equals()方法都继承自Object类
在没有重写equals()方法的类中,调用equals()方法其实和使用==的效果一样,也是比较的是引用类型的变量所指向的对象的地址
不过,Java提供的类中,有些类都重写了equals()方法,重写后的equals()方法一般都是比较两个对象的值,比如String类。
Object类equals()方法源码:
public boolean equals(Object obj) {
return (this == obj);
}
String类equals()方法源码:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?
https://blog.csdn.net/weixin_38405253/article/details/91922340
不一定
HashCode
的存在是为了查找的快捷性,用于散列存储结构中确定对象的储存地址。- 如果两个对象
equals
相等,则HashCode
也一定相等 - 如果equals方法被重写,则HashCode也应该被重写(这是为了避免出现两个对象相同,但是hashCode值却不同的情况)
- 如果两个对象的HashCode相等,equals方法不一定相等
HashCode是Object的方法。查看Object源码,找到HashCode,可以看到返回值是int,是native方法,不是Java原生方法,是由其他语言编写的。equals返回值是布尔值,默认实现是==。
严格来讲他们两个直接没有任何关系,因为我们可以随意的重写这两个方法的实现。
定义对象H,他的hashCode就是固定的返回一个值1。这样的话我们创建两个对象,他们的hashCode一定是相等的;而他的equals因为是两个对象,内存地址不一样,所以返回false。
class H{
@Override
publice int hashCode(){
return 1;
}
}
H h1 = new H();
H h2 = new H();
System.out.println(h1.hashCode() == h2.hashCode()); //true
System.out.println(h1.equals(h2)); //false
另一方面,如果我们重写的是equals方法,并且返回固定值true。我们同样创建两个对象,他们的hashCode返回值是false,但是一定equals。因为重写了equals方法。
class H{
@Override
publice boolean equals(Object o){
return true;
}
}
H h1 = new H();
H h2 = new H();
System.out.println(h1.hashCode() == h2.hashCode()); //false
System.out.println(h1.equals(h2)); //true
上面这两个例子可以看出,hashcode和equals其实没有必然的联系。
规范
但是在重写hashCode和equals方法的时候,我们应该满足这样的规范。
- hashCode和equals返回值应该是稳定的,不应有随机性。
- 俩对象==返回true则这两个对象的equals也应该返回true。
- 俩对象equals则这两个对象的hashCode应该相等。
// 不规范但是能照常运行
class H{
@Override
publice int hashCode(){
return Math.random()>0.5 ? 2:1;
}
}
为什么要有这样的规范呢?这就涉及到了hash存储,以hashMap为例:
在hashMap中,我们通过hashCode得到的值来计算出在数组中的下标。
例如下标等于1,那么就在1这个桶中去找对应的元素;1这个桶中存储的可能是链表或者是红黑树;假如说是一个链表,我们就要在这个链式结构中挨个去查找,是否是我们想要的元素,而挨个查找的时候我们就需要运用equals方法,对比每个节点的key跟我们要找的key是否相等。
通俗的讲,hashMap的查找或者插入过程中我们使用hashCode来确定一个固定的桶,确定了桶范围之后,我们再使用equals方法在这个桶内,可能是链表或是红黑树中去找到真正对应的元素。
这也就是为什么需要这样的规范,如果hashCode和equals不稳定,那么我们在每次查找的时候,可能就到不同的桶里面去。如果两个对象equals但两个对象hashCode不相等的话,就导致我们在两次求这个index下标的时候求得不同的下标,可能就找不到对应的值。
final 在 java 中有什么作用?
https://www.cnblogs.com/tanshaoshenghao/p/10908771.html
final作为Java中的关键字可以用于三个地方。用于修饰类、类属性和类方法。
特征:凡是引用final关键字的地方不可修改。
- 修饰类:表示该类不能被继承
- 修饰方法:表示方法不能被重写
- 修饰变量:表示变量只能一次赋值以后值不能被修改(常量)
被final修饰的变量是不能够被改变的。但是这里的"不能够被改变"对于不同的数据类型是有不同的含义的。
当final修饰的是一个基本数据类型数据时,这个数据的值在初始化后将不能被改变
当final修饰的是一个引用类型数据时,也就是修饰一个对象时,引用在初始化后将永远指向一个内存地址,不可修改。但是该内存地址中保存的对象信息,是可以进行修改的。
final Person p = new Person(20, "ABCD");
p.setAge(18); //可以修改p对象的数据
System.out.println(p.getAge()); //输出18
Person pp = new Person(30, "ABCD");
p = pp; //这行代码会报错, 不能通过编译, 因为p经final修饰永远指向上面定义的p对象, 不能指向pp对象.
final修饰变量的本质: final修饰的变量会指向一块固定的内存,这块内存中的值不能改变。
引用类型变量所指向的对象之所以可以修改,是因为引用变量不是直接指向对象的数据,而是指向对象的引用的。
所以被final修饰的引用类型变量将永远指向一个固定的对象,不能被修改;对象的数据值可以被修改。
被final修饰的常量在编译阶段会被放入常量池中
final是用于定义常量的,定义常量的好处是:不需要重复地创建相同的变量。而常量池是Java的一项重要技术,由final修饰的变量会在编译阶段放入到调用类的常量池中
int n1 = 2019; //普通变量
final int n2 = 2019; //final修饰的变量
String s = "20190522";
String s1 = n1 + "0522"; //拼接字符串"20190512"
String s2 = n2 + "0522";
System.out.println(s == s1); //false
System.out.println(s == s2); //true
- 上面的代码运作过程是这样的:
- 首先根据final修饰的常量会在编译期放到常量池的原则, n2会在编译期间放到常量池中.
- 然后s变量所对应的"20190522"字符串会放入到字符串常量池中, 并对外提供一个引用返回给s变量.
- 这时候拼接字符串s1, 由于n1对应的数据没有放入常量池中, 所以s1暂时无法拼接, 需要等程序加载运行时才能确定s1对应的值.
- 但在拼接s2的时候, 由于n2已经存在于常量池, 所以可以直接与"0522"拼接, 拼接出的结果是"20190522". 这时系统会查看字符串常量池, 发现已经存在字符串20190522, 所以直接返回20190522的引用. 所以s2和s指向的是同一个引用, 这个引用指向的是字符串常量池中的20190522.
当程序执行时。n1变量才有具体的指向。
当拼接s1的时候,会创建一个新的String类型对象,也就是说字符串常量池中的20190522会对外提供一个新的引用。
所以当s1与s用"=="判断时,由于对应的引用不同,会返回false。而s2和s指向同一个引用,返回true。
这个例子想说明的是: 由于被final修饰的常量会在编译期进入常量池,如果有涉及到该常量的操作,很有可能在编译期就已经完成。
final修饰方法
使用final修饰方法有两个作用,首要作用是锁定方法,不让任何继承类对其进行修改。
另外一个作用是在编译器对方法进行内联, 提升效率。但是现在已经很少这么使用了,近代的Java版本已经把这部分的优化处理得很好了。但是还是了解一下什么是方法内敛。
方法内敛:当调用一个方法时,系统需要进行保存现场信息,建立栈帧,恢复线程等操作,这些操作都是相对比较耗时的。如果使用final修饰一个了一个方法a,在其他调用方法a的类进行编译时,方法a的代码会直接嵌入到调用a的代码块中。
//原代码
public static void test(){
String s1 = "包夹方法a";
a();
String s2 = "包夹方法a";
}
public static final void a(){
System.out.println("我是方法a中的代码");
System.out.println("我是方法a中的代码");
}
//经过编译后
public static void test(){
String s1 = "包夹方法a";
System.out.println("我是方法a中的代码");
System.out.println("我是方法a中的代码");
String s2 = "包夹方法a";
}
在方法非常庞大的时候,这样的内嵌手段是几乎看不到任何性能上的提升的,在最近的Java版本中,不需要使用final方法进行这些优化了。
final修饰类
使用final修饰类的目的简单明确:表明这个类不能被继承
当程序中有永远不会被继承的类时,可以使用final关键字修饰
被final修饰的类所有成员方法都将被隐式修饰为final方法
String 属于基础的数据类型吗?
String不是基本的数据类型,是final修饰的java类,java中的基本类型一共有8个,它们分别为:
- 字符类型:byte,char
- 基本整型:short,int,long
- 浮点型:float,double
- 布尔类型:boolean
但是为什么会有这么一问呢?
- 当我们在给一个String类型的变量赋值的时候我们是可以不使用 “new” 关键字的, 例如: String name = “小明”; 这样就ok了
- 这种写法和基本数据类型的使用是很像的
- java中的设计,String类型的值是存储在常量池中的。String类型是我们在编程中经常使用的数据类型。因此java的设计者将String类型做了一定的特殊处理。
测试代码:
String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
System.out.println(s1.hashCode()); // 96354
System.out.println(s2.hashCode()); // 96354
System.out.println(s3.hashCode()); // 96354
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
System.out.println(s1.equals(s3)); //true
可以知道
s1
和s2
和s3
最终存储的都是同一个对象,因为他们的hashCode码是相同的。s1
和s2
不但最终存储的对象是相同的,而且直接指向的对象地址也是相同的,因为使用==返回的是true。s1
和s3
虽然最终存储的对象是同一个对象,但是他们的地址是不同的,因为打印出来的hashCode码相同但是使用==返回的是false。
常量池
https://blog.csdn.net/qq_41376740/article/details/80338158
https://www.cnblogs.com/syp172654682/p/8082625.html
Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。
所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量和符号引用量,字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
- 类和接口的全限定名
- 字段名称和描述符
- 方法名称和描述符
而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()
方法。
String的intern()
方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。
常量池的好处
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
- 节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
- 节省运行时间:比较字符串时,
==
比equals()
快。对于两个引用变量,只用==
判断引用是否相等,也就可以判断实际值是否相等。
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // true
System.out.println(s1 == s4); // false
System.out.println(s1 == s9); // false
System.out.println(s4 == s5); // false
System.out.println(s1 == s6); // true
分析:
1、s1 == s2
很容易可以判断出来。s1 和 s2 都指向了方法区常量池中的Hello。
2、s1 == s3
这里要注意一下,因为做+号的时候,会进行优化,自动生成Hello赋值给s3,所以也是true
3、s1 == s4
s4是分别用了常量池中的字符串和存放对象的堆中的字符串,做+的时候会进行动态调用,最后生成的仍然是一个String对象存放在堆中。
4、s1 == s9
在JAVA9中,因为用的是动态调用,所以返回的是一个新的String对象。所以s9和s4,s5这三者都不是指向同一块内存。
5、s1 == s6
为啥s1 和 s6地址相等呢? 归功于intern方法,这个方法首先在常量池中查找是否存在一份equal相等的字符串如果有的话就返回该字符串的引用,没有的话就将它加入到字符串常量池中,所以存在于class中的常量池并非固定不变的,可以用intern方法加入新的。
如何将字符串反转?
https://www.cnblogs.com/binye-typing/p/9260994.html
利用 StringBuffer 或 StringBuilder 的 reverse
成员方法:
// StringBuffer
public static String reverse1(String str){
return new StringBuilder(str).reverse().toString();
}
利用 String 的 toCharArray
方法先将字符串转化为 char 类型数组,然后将各个字符进行重新拼接:
// toCharArray
public static String reverse2(String str) {
char[] chars = str.toCharArray();
String reverse = "";
for (int i = chars.length - 1; i >= 0; i--) {
reverse += chars[i];
}
return reverse;
}
利用 String 的 CharAt
方法取出字符串中的各个字符:
// charAt
public static String reverse3(String str) {
String reverse = "";
int length = str.length();
for (int i = 0; i < length; i++) {
reverse = str.charAt(i) + reverse;
}
return reverse;
}
String 类的常用方法都有那些?
length()
:得到一个字符串的字符个数isEmpty()
:判断字符串的长度是否为空charAt
:得到指定下标位置对应的字符toCharArray()
:将一个字符串转换成字符数组split(String)
:将一个字符串按照指定内容劈开equals()
:判断两个字符串的内容是否一样equalsIsIgnoreCase(String)
:忽略大小写的比较两个字符串的内容是否一样contains(String)
:判断一个字符串里面是否包含指定的内容startsWith(String)
:判断一个字符串是否以指定的内容开头endsWith(String)
:判断一个字符串是否以指定的内容结尾toUpperCase()
:将一个字符串全部转换成大写toLowerCase()
:将一个字符串全部转换成小写replace(String,String)
:将某个内容全部替换成指定内容replaceAll(String,String)
:将某个内容全部替换成指定内容,支持正则repalceFirst(String,String)
:将第一次出现的某个内容替换成指定的内容substring(int)
:从指定下标开始一直截取到字符串的最后substring(int,int)
:从下标x截取到下标y-1对应的元素trim()
:去除一个字符串的前后空格indexOf(String)
:得到指定内容第一次出现的下标lastIndexOf(String)
:得到指定内容最后一次出现的下标
Array和ArrayList有什么区别?
- Array类型的变量在声明的同时必须进行实例化(至少得初始化数组的大小)
- ArrayList可以只是先声明。
int[] array = new array[3];
int[] array = {1,2,3};
// 而直接使用 int[] array; 是不行的。
ArrayList myList = new ArrayList();
Array只能存储同构的对象,而ArrayList可以存储异构的对象。
- 同构的对象是指类型相同的对象,若声明为
int[]
的数组就只能存放整形数据,string[]
只能存放字符型数据,但声明为object[]
的数组除外。 - ArrayList可以存放任何不同类型的数据。
Array是始终是连续存放的,而ArrayList的存放不一定连续。
Array对象的初始化必须只定指定大小,且创建后的数组大小是固定的。ArrayList的大小可以动态指定,其大小可以在初始化时指定,也可以不指定,也就是说该对象的空间可以任意增加。
Array和ArrayList的相似点
都具有索引(index),即可以通过index来直接获取和修改任意项。
他们所创建的对象都放在托管堆中。
都能够对自身进行枚举(因为都实现了IEnumerable接口)。