「JAVA」对象中使用 static 和 String 的一些基础探究
yuyutoo 2024-10-12 00:57 3 浏览 0 评论
前言
跟同学在讨论 JAVA 期末试题时,对于一些 static 和 String 在对象中的使用方法,若有所思,特此记录一下,也祝没有对象的友友可以自己 new 一个出来!
那我们先来看一看试卷里的原题;
原题
主要就是两个类 MyClass.java 和 TestMyClass.java,填代码的部分就直接跳过了,然后就是输出结果,看看你是否也能全部正确,两个类的具体代码如下:
MyClass.java
public class MyClass {
private int count;
String info;
public static String message = "Good";
public MyClass increase() {
count++;
return this;
}
private MyClass() {
this.count=0;
this.info = "GoodLuck";
}
public int getCount() {
return count;
}
public static MyClass getInstance() {
return new MyClass();
}
}
复制代码
TestMyClass.java
public class TestMyClass {
public static void main(String[] args) {
MyClass mc1 = MyClass.getInstance();
MyClass mc2 = MyClass.getInstance();
mc1.message = "Great";
mc2.message = "Excellent";
MyClass.message = "Nice";
System.out.println(mc1.message+":"+mc2.message+":"+MyClass.message);
System.out.println(mc1.info == mc2.info);
mc2.info = new String("GoodLuck");
System.out.println(mc1.info == mc2.info);
System.out.println(mc1.info.equals(mc2.info));
System.out.println(mc1.increase().increase().getCount());
}
}
复制代码
运行结果:
Nice:Nice:Nice
true
false
true
2
复制代码
如果你全部答对了,那么恭喜你,基础很不错哟;答错的小伙伴也不要丧气,接下来听我娓娓道来,扎实基础;
static
工欲善其事必先利其器,在开始解析之前,我们先回顾一下一些关于 static 的知识;
简介
static 表示 “全局” 或者 “静态” 的意思,用来修饰成员变量和成员方法,也可以形成静态 static 代码块,但是 Java 语言中没有全局变量的概念;
被 static 修饰的成员变量和成员方法独立于该类的任何对象,也就是说,它不依赖类特定的实例,被类的所有实例共享;
只要这个类被加载,Java 虚拟机就能根据类名在运行时数据区的方法区内定找到他们,因此,static 对象可以在它的任何对象创建之前访问,无需引用任何对象;
用 public 修饰的 static 成员变量和成员方法本质是全局变量和全局方法,当声明它类的对象时,不生成 static 变量的副本,而是类的所有实例共享同一个 static 变量;
static 变量前可以有 private 修饰,表示这个变量可以在类的静态代码块中,或者类的其他静态成员方法中使用,但是不能在其他类中通过类名来直接引用,这一点很重要;
实际上你需要搞明白,private 是访问权限限定,static 表示不要实例化就可以使用,这样就容易理解多了,static 前面加上其它访问权限关键字的效果也以此类推。
static 修饰的成员变量和成员方法习惯上称为静态变量和静态方法,可以直接通过类名来访问,访问语法为:
类名.静态方法名(参数列表…)
复制代码
使用
回顾了 static 相关知识之后,我们来看一下题目中的使用吧;
// MyClass.java
public static String message = "Good";
// TestMyClass.java
MyClass mc1 = MyClass.getInstance();
MyClass mc2 = MyClass.getInstance();
mc1.message = "Great";
mc2.message = "Excellent";
MyClass.message = "Nice";
System.out.println(mc1.message+":"+mc2.message+":"+MyClass.message);
复制代码
先是用 static 修饰了成员变量 message,然后通过下断点调试可以获知,两个对象 mcl1 和 mcl2 被分配到了两个不同的地址;
在往下调试时,发现 mc1.message,mc2.message,MyClass.message 三个成员变量的值是一样的,且都从 Great → Excellent → Nice,这就是刚才所回顾的,被 static 修饰的成员变量成了共享变量,被类的所有实例共享;
接下来我们再做个试验验证一下:
//修改前
private int count;
//修改后
private static int count;
复制代码
可以发现,我们只是对 mcl1 对象进行了操作,但是 mcl2 的成员变量 count 也跟着改变了,这就是因为在 MyClass 类中,成员变量 count 被 static 修饰,已经成了该类的共享变量了,但凡是该类的对象,都访问的是同一个 count 变量;
当然也可以通过身份码进行验证:
System.out.println("mcl1: " + System.identityHashCode(mc1));
System.out.println("mcl2: " + System.identityHashCode(mc2));
System.out.println("mcl1_count: " + System.identityHashCode(mc1.getCount()));
System.out.println("mcl2_count: " + System.identityHashCode(mc2.getCount()));
复制代码
mcl1: 940553268
mcl2: 1720435669
mcl1_count: 1020923989
mcl2_count: 1020923989
复制代码
因此 System.out.println(mc1.message+":"+mc2.message+":"+MyClass.message); 输出的是 Nice:Nice:Nice;
接下来讲一些关于 String 的小知识;
String
关于 String 的话,这里用到啥聊啥,就不全面的进行了;
== 与 equals()
先来讲讲关于 String 的比较,一般常见的比较有两种,即 == 和 equals();
其中,== 比较的是两个字符串的地址是否为相等(同一个地址),equals() 方法比较的是两个字符串对象的内容是否相同(当然,若两个字符串引用同一个地址,使用 equals() 比较也返回 true);
这里就不得不提第二个知识点了,String 常量与非常量的区别;
常量与非常量
那什么是常量,什么是非常量呢,简单了解就是,String name = "sid10t." 这个是常量,属于是对 name 进行赋值,直接存储在常量池中,而 String name = new String("sid10t.") 这个就是非常量,因为重新创建了一个对象,这会将字符串 sid10t. 存储在常量池中,然后在 Heap 中创建对象指向 name;
那这里为什么要提这个呢?当然是因为他们有较大的区别;
在 Java 语言规范(JavaSE 1.8版本)章节3.10.5 中有做规范,所有的 Java 语言编译、运行时环境实现都必须依据此规范来实现,里面有这么一句话:
Moreover, a string literal always refers to the same instance of class String. This is because string literals - or, more generally, strings that are the values of constant expressions (§15.28) - are "interned" so as to share unique instances, using the method String.intern.
大致意思就是凡是内容一样的字符串常数,都要引用同一个字符串对象,换句话说就是内存地址相同;
因为其值为常量的字符串,都会通过 String.intern() 函数被限定为共享同一个对象;
稍后会解析 intern() 函数,也可以自行参考说明 String (Java Platform SE 8 );
回到正题,看一下语言规范里的这段代码:
package testPackage;
class Test {
public static void main(String[] args) {
String hello = "Hello", lo = "lo";
System.out.print((hello == "Hello") + " ");
System.out.print((Other.hello == hello) + " ");
System.out.print((other.Other.hello == hello) + " ");
System.out.print((hello == ("Hel"+"lo")) + " ");
System.out.print((hello == ("Hel"+lo)) + " ");
System.out.println(hello == ("Hel"+lo).intern());
}
}
class Other { static String hello = "Hello"; }
复制代码
以及另一个包中的类:
package other;
public class Other { public static String hello = "Hello"; }
复制代码
运行结果:
true true true true false true
复制代码
结论:
- 同一个包中同一个类中的字符串表示对同一个 String 对象的引用;
- 同一个包中不同类中的字符串表示对同一个 String 对象的引用;
- 不同包中不同类中的字符串同样表示对同一 String 对象的引用;
- 由常量表达式计算的字符串在编译时计算,然后将其视为文字;
- 在运行时通过连接计算的字符串是新创建的,因此是不同的;
- 显式地嵌入一个计算出来的字符串的结果与任何现有的具有相同内容的字面值字符串的结果相同;
如果对结论的理解不是很深刻的话,那就看看接下来的解释:
System.out.print((hello == "Hello") + " ");
System.out.print((Other.hello == hello) + " ");
System.out.print((other.Other.hello == hello) + " ");
复制代码
“Hello” 和 “lo” 是字符串常量,在编译期就被确定了,先检查字符串常量池中是否含有 “Hello” 和 “lo”,如果没有则添加 “Hello” 和 “lo” 到字符串常量池中,并且直接指向它们,所以 hello 和 lo 分别直接指向字符串常量池的 “Hello” 和 “lo”,也就是 hello 和 lo 指向的地址分别是常量池中的 “Hello” 和 “lo” ,因此第一个输出中 hello 实际就是 “Hello”,所以 "Hello" == "Hello" 为 true,前三个输出都是同理的;
System.out.print((hello == ("Hel"+"lo")) + " ");
复制代码
"Hel" 和 "lo" 都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,在编译器会被编译器优化成 "Hello",因为 "Hello" 在常量池中了,因此输出为 true;
System.out.print((hello == ("Hel"+lo)) + " ");
System.out.println(hello == ("Hel"+lo).intern());
复制代码
JVM 对于字符串引用,由于在字符串的 + 连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即 "Hel"+lo,所以在不执行 intern() 方法的前提下,"Hel"+lo 不会存到常量池中,但 "Hel" 会被存到常量池中去,所以输出一个为 true,一个为 false;
intern()
String.intern() 是一个 Native 方法,它的作用是如果字符串常量池已经包含一个等于此 String 对象的字符串,则返回字符串常量池中这个字符串的引用, 否则将当前 String 对象的引用地址(堆中)添加到字符串常量池中并返回。
JAVA 源码
/*
Returns a canonical representation for the string object.
A pool of strings, initially empty, is maintained privately by the class String.
When the intern method is invoked, if the pool already contains a string equal to
this String object as determined by the equals(Object) method, then the string from
the pool is returned. Otherwise, this String object is added to the pool and a
reference to this String object is returned.
It follows that for any two strings s and t, s.intern() == t.intern() is true if and
only if s.equals(t) is true.
All literal strings and string-valued constant expressions are interned. String
literals are defined in section 3.10.5 of the The Java Language Specification.
Returns:
a string that has the same contents as this string, but is guaranteed to be from a
pool of unique strings.
*/
public native String intern();
复制代码
native 源码
String.c
Java_java_lang_String_intern(JNIEnv *env, jobject this)
{
return JVM_InternString(env, this);
}
复制代码
jvm.h
/*
* java.lang.String
*/
JNIEXPORT jstring JNICALL
JVM_InternString(JNIEnv *env, jstring str);
复制代码
jvm.cpp
JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
JVMWrapper("JVM_InternString");
JvmtiVMObjectAllocEventCollector oam;
if (str == NULL) return NULL;
oop string = JNIHandles::resolve_non_null(str);
oop result = StringTable::intern(string, CHECK_NULL);
return (jstring) JNIHandles::make_local(env, result);
JVM_END
复制代码
symbolTable.cpp
oop StringTable::intern(Handle string_or_null, jchar* name,
int len, TRAPS) {
unsigned int hashValue = java_lang_String::hash_string(name, len);
int index = the_table()->hash_to_index(hashValue);
oop string = the_table()->lookup(index, name, len, hashValue);
// Found
if (string != NULL) return string;
// Otherwise, add to symbol to table
return the_table()->basic_add(index, string_or_null, name, len,
hashValue, CHECK_NULL);
}
oop StringTable::lookup(int index, jchar* name,
int len, unsigned int hash) {
for (HashtableEntry<oop>* l = bucket(index); l != NULL; l = l->next()) {
if (l->hash() == hash) {
if (java_lang_String::equals(l->literal(), name, len)) {
return l->literal();
}
}
}
return NULL;
}
复制代码
它的大体实现结构就是:JAVA 使用 jni 调用 c++ 实现的 StringTable 的 intern 方法,StringTable 的 intern 方法跟 Java 中的 HashMap 的实现是差不多的,只是不能自动扩容,默认大小是1009。
要注意的是,String 的 String Pool 是一个固定大小的 Hashtable,默认值大小长度是1009,如果放进 String Pool 的 String 非常多,就会造成 Hash 冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用 String.intern 时性能会大幅下降(因为要一个一个找)。
在 JDK6 中 StringTable 是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。
在 JDK7 中,StringTable 的长度可以通过一个参数指定:-XX:StringTableSize=99991;
使用
在 JDK1.7 之前的版本,调用这个方法的时候,会去常量池中查看是否已经存在这个常量了,如果已经存在,那么直接返回这个常量在常量池中的地址值,如果不存在,则在常量池中创建一个,并返回其地址值。
但是在 JDK1.7 以及之后的版本中,常量池从 perm 区搬到了 heap 区。intern() 检测到这个常量在常量池中不存在的时候,不会直接在常量池中创建该对象了,而是将堆中的这个对象的引用直接存到常量池中,减少内存开销。
来看这段代码:
public static void main(String[] args) {
// part1
String s1 = new String("sid10t.");
s1.intern();
String s2 = "sid10t.";
System.out.println(s1 == s2);
// part2
String s3 = new String("Hello ") + new String("World!");
s3.intern();
String s4 = "Hello World!";
System.out.println(s3 == s4);
}
复制代码
在 JDK7 之前两个都是 false,在 JDK7 之后输出分别是 false,true;
接下来根据 JDK7 进行分析,
先来看 part1 部分:
String s1 = new String("sid10t."); 这行代码生成了两个最终对象:一个是常量池中的字符串常量 sid10t.,另一个是在堆中的 s1 引用指向的对象;
然后是第二行 s1.intern();,返回常量池中的字符串常量 sid10t.,因为常量池中已经存在了该常量,所以这里就直接返回即可,因此,在 part1 的此情此景中,这句话可写可不写,对输出结果没有任何影响;
所以最后输出的肯定是 false,一个地址在堆中,一个在常量池里;
接下来看看 part2 部分:
String s3 = new String("Hello ") + new String("World!"); 这行代码生成了三个最终对象:两个分别是常量池中的对象 Hello ,World!,还有一个在堆中的 s3 引用指向的对象;
然后是第二行 s3.intern();,由于现在的常量池中不存在字符串常量 Hello World!,因此它会直接存储 s3 在堆中的引用地址,而不是拷贝一份;
这时候,String s4 = "Hello World!";,常量池中已经有了 Hello World! 常量,也就是 s3 的引用地址,因此 s4 的值就是 s3 的引用地址,所以输出的是 true;
根据上述分析,我们将 part2 的代码略作调整,如下:
String s3 = new String("Hello ") + new String("World!");
// s3.intern();
String s4 = "Hello World!";
s3.intern();
System.out.println(s3 == s4);
复制代码
输出的就是 false 了,但如果是 s3.intern() == s4,则输出的就是 true;
想必你应该理解了!
后记
帮助别人也是帮助自己啊,所以要经常助人为乐,总的来说,还是收获颇丰的,不仅巩固了知识,还在其中发现和理解了以前不知道的细节,真是温故而知新 ,可以为师矣;
原文链接:https://juejin.cn/post/7147207839562334215
相关推荐
- jQuery VS AngularJS 你更钟爱哪个?
-
在这一次的Web开发教程中,我会尽力解答有关于jQuery和AngularJS的两个非常常见的问题,即jQuery和AngularJS之间的区别是什么?也就是说jQueryVSAngularJS?...
- Jquery实时校验,指定长度的「负小数」,小数位未满末尾补0
-
在可以输入【负小数】的输入框获取到焦点时,移除千位分隔符,在输入数据时,实时校验输入内容是否正确,失去焦点后,添加千位分隔符格式化数字。同时小数位未满时末尾补0。HTML代码...
- 如何在pbootCMS前台调用自定义表单?pbootCMS自定义调用代码示例
-
要在pbootCMS前台调用自定义表单,您需要在后台创建表单并为其添加字段,然后在前台模板文件中添加相关代码,如提交按钮和表单验证代码。您还可以自定义表单数据的存储位置、添加文件上传字段、日期选择器、...
- 编程技巧:Jquery实时验证,指定长度的「负小数」
-
为了保障【负小数】的正确性,做成了通过Jquery,在用户端,实时验证指定长度的【负小数】的方法。HTML代码<inputtype="text"class="forc...
- 一篇文章带你用jquery mobile设计颜色拾取器
-
【一、项目背景】现实生活中,我们经常会遇到配色的问题,这个时候去百度一下RGB表。而RGB表只提供相对于的颜色的RGB值而没有可以验证的模块。我们可以通过jquerymobile去设计颜色的拾取器...
- 编程技巧:Jquery实时验证,指定长度的「正小数」
-
为了保障【正小数】的正确性,做成了通过Jquery,在用户端,实时验证指定长度的【正小数】的方法。HTML做成方法<inputtype="text"class="fo...
- jquery.validate检查数组全部验证
-
问题:html中有多个name[],每个参数都要进行验证是否为空,这个时候直接用required:true话,不能全部验证,只要这个数组中有一个有值就可以通过的。解决方法使用addmethod...
- Vue进阶(幺叁肆):npm查看包版本信息
-
第一种方式npmviewjqueryversions这种方式可以查看npm服务器上所有的...
- layui中使用lay-verify进行条件校验
-
一、layui的校验很简单,主要有以下步骤:1.在form表单内加上class="layui-form"2.在提交按钮上加上lay-submit3.在想要校验的标签,加上lay-...
- jQuery是什么?如何使用? jquery是什么功能组件
-
jQuery于2006年1月由JohnResig在BarCampNYC首次发布。它目前由TimmyWilson领导,并由一组开发人员维护。jQuery是一个JavaScript库,它简化了客户...
- django框架的表单form的理解和用法-9
-
表单呈现...
- jquery对上传文件的检测判断 jquery实现文件上传
-
总体思路:在前端使用jquery对上传文件做部分初步的判断,验证通过的文件利用ajaxFileUpload上传到服务器端,并将文件的存储路径保存到数据库。<asp:FileUploadI...
- Nodejs之MEAN栈开发(四)-- form验证及图片上传
-
这一节增加推荐图书的提交和删除功能,来学习node的form提交以及node的图片上传功能。开始之前需要源码同学可以先在git上fork:https://github.com/stoneniqiu/R...
- 大数据开发基础之JAVA jquery 大数据java实战
-
上一篇我们讲解了JAVAscript的基础知识、特点及基本语法以及组成及基本用途,本期就给大家带来了JAVAweb的第二个知识点jquery,大数据开发基础之JAVAjquery,这是本篇文章的主要...
- 推荐四个开源的jQuery可视化表单设计器
-
jquery开源在线表单拖拉设计器formBuilder(推荐)jQueryformBuilder是一个开源的WEB在线html表单设计器,开发人员可以通过拖拉实现一个可视化的表单。支持表单常用控件...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)