Java 利用万物皆对象的思想和单一一致的语法方式来简化问题。虽万物皆可为对象,但我们所操纵的标识符实际上只是对对象的“引用” 。 举例:我们可以用遥控器(引用)去操纵电视(对象)。只要拥有对象的“引用”,就可以操纵该“对象”。换句话说,我们无需直接接触电视,就可通过遥控器(引用)自由地控制电视(对象)的频道和音量。此外,没有电视,遥控器也可以单独存在。就是说,你仅仅有一个“引用”并不意味着你必然有一个与之关联的“对象”。
这里我们只是创建了一个 String 对象的引用,而非对象。直接拿来使用会出现错误:Variable ‘str’ might not have been initialized(变量’str’可能没有初始化)
public static void main(String[] args) {
String str;
System.out.println(str);
/* erro
Variable 'str' might not have been initialized
*/
}
通常更安全的做法是:创建一个引用的同时进行初始化。Java 语法允许我们使用带双引号的文本内容来初始化字符串且存放于栈内存中。同样,其他类型的对象也有相应的初始化方式。
public static void main(String[] args) {
String str = "hello world";
System.out.println(str);
}
“引用”用来关联“对象”。在 Java 中,通常我们使用new操作符来创建一个新对象。new 关键字代表:创建一个新的对象实例。
public static void main(String[] args) {
String str = new String("hello world");
System.out.println(str);
}
除了 String 类型以外,Java 本身自带了许多现成的数据类型。除此之外,我们还可以创建自己的数据类型。事实上,这是 Java 程序设计中的一项基本行为。
如果一切都是对象,那么是什么决定了某一类对象的外观和行为呢?换句话说,是什么确定了对象的类型?你可能很自然地想到 type 关键字。但是,事实上大多数面向对象的语言都使用 class 关键字类来描述一种新的对象。 通常在 class 关键字的后面的紧跟类的的名称。如下代码示例:
/**
* 国家类
*/
public class State{
}
当我们创建好一个类之后,我们可以往类里存放两种类型的元素:方法(method)和字段(field)。类的字段可以是基本类型,也可以是引用类型。如果类的字段是对某个对象的引用,那么必须要初始化该引用将其关联到一个实际的对象上(通过之前介绍的创建对象的方法)。每个对象都有用来存储其字段的空间。通常,字段不在对象间共享。下面是一个具有某些字段的类的代码示例:
/**
* 国家类
*/
public class State {
//国家名称
private String stateName;
public void setStateName(String stateName){
this.stateName = stateName;
}
public String getStateName(){
return this.stateName;
}
}
我们可以通过new的方式来创建它的一个对象并且可以通过对象名称.变量名称方式给这个类的属性赋值:
public static void main(String[] args) {
State state = new State();
state.stateName = "梵蒂冈";
}
大多数程序语言都有作用域的概念。作用域决定了在该范围内定义的变量名的可见性和生存周期。例如:gdp可以访问State类下面的全局变量stateName,但是State类访问不了gdp方法里面的局部变量gdp参数且他们的变量名不能相同,这是因为Java 的变量只有在其作用域内才可用,作用域是由大括号 {} 的位置决定的。
全局变量:全局变量是可以被本程序所有对象或函数引用。
局部变量:类的方法中的变量。只能在方法作用域内被使用
public class State {
//国家名称
private String stateName;
public void gdp(){
this.stateName = "中国";
int gdp = 65000;
}
在许多语言(如 C 和 C++)中,使用术语 函数 (function) 用来命名子程序。在 Java 中,我们使用术语 方法(method)来表示“做某事的方式”。
在 Java 中,方法决定对象能接收哪些消息。方法的基本组成部分包括名称、参数、返回类型、方法体。格式如:
[返回类型] [方法名](/*参数列表*/){
// 方法体
}
返回类型:表明了当你调用它时会返回的结果类型,通过给方法标识 void 来表明这是一个无需返回值的方法。
参数列表:指定了传递给方法的信息。正如你可能猜到的,这些信息就像 Java 中的其他所有信息 ,以对象的形式传递。参数列表必须指定每个对象的类型和名称。同样,我们并没有直接处理对象,而是在传递对象引用。但是引用的类型必须是正确的。如果方法需要 String 参数,则必须传入 String,否则编译器将报错。
public void setStateName(String stateName){
this.stateName = stateName;
}
public String getStateName(){
return this.stateName;
}
Java 中的方法只能作为类的一部分创建。它只能被对象所调用 ,并且该对象必须有权限来执行调用。若对象调用错误的方法,则程序将在编译时报错。我们可以像下面这样调用一个对象的方法:
[对象引用].[方法名](参数1, 参数2, 参数3);
可以通过调用setStateName()方法来进行赋值,调用getStateName()方法获取值:
public static void main(String[] args) {
State state = new State();
state.setStateName("中国");
String stateName = state.getStateName();
System.out.println(stateName);//中国
}
知道了对象的创建就必须聊一聊对象的清理,在一些编程语言中,管理变量的生命周期需要大量的工作。一个变量需要存活多久?如果我们想销毁它,应该什么时候去做呢?变量生命周期的混乱会导致许多 bug,本小结向你介绍 Java 是如何通过释放存储来简化这个问题的。
{
String str = new String("Hello world");
}
// 作用域终点
上例中,引用 str 在作用域(作用域是由大括号 {} 的位置决定的)终点就结束了。但是,引用str指向的字符串对象依然还在占用内存。在这段代码中,我们无法在这个作用域之后访问这个对象,因为唯一对它的引用 str 已超出了作用域的范围。
只要你需要,new 出来的对象就会一直存活下去。 相比在 C++ 编码中操作内存可能会出现的诸多问题,这些困扰在 Java 中都不复存在了。在 C++ 中你不仅要确保对象的内存在你操作的范围内存在,还必须在使用完它们之后,将其销毁。
那么问题来了:我们在 Java 中并没有主动清理这些对象,那么它是如何避免 C++ 中出现的内存被填满从而阻塞程序的问题呢?答案是:Java 的垃圾收集器会检查所有 new 出来的对象并判断哪些不再可达,继而释放那些被占用的内存,供其他新的对象使用。也就是说,我们不必担心内存回收的问题了。你只需简单创建对象即可。当其不再被需要时,能自行被垃圾收集器释放。垃圾回收机制有效防止了因程序员忘记释放内存而造成的“内存泄漏”问题。
寄存器(Registers):最快的存储区域,位于 CPU 内部 ,存储二进制代码。无法对它直接操作,程序里也看不到任何存在的踪迹。
栈内存(Stack)存在于常规内存 RAM(随机访问存储器,Random Access Memory)区域中。存取速度比堆要快,仅次于直接位于CPU中的寄存器,一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配,内存空间有限,由操作系统自动分配和释放,是一种先进后出顺序排列的数据结构。
堆内存(Heap)这是一种通用的内存池(也在 RAM 区域),所有 Java 对象都存在于其中。堆内存用来存放由 new 创建的对象和数组,在堆中分配的内存,几乎没有空间限制。由 Java 虚拟机的自动垃圾回收器来管理,是一种先进先出顺序排列的数据结构。
常量存储(Constant storage)常量值通常直接放在程序代码中,因为它们永远不会改变。
非 RAM 存储(Non-RAM storage)数据完全存在于程序之外,在程序未运行以及脱离程序控制后依然存在。两个主要的例子:
1.序列化对象:对象被转换为字节流,通常被发送到另一台机器。
2.持久化对象:对象被放置在磁盘上,即使程序终止,数据依然存在。这些存储的方式都是将对象转存于另一个介质中,并在需要时恢复成常规的、基于 RAM 的对象。Java 为轻量级持久化提供了支持。而诸如 JDBC 和 Hibernate 这些类库为使用数据库存储和检索对象信息提供了更复杂的支持。
基本类型在 Java 中使用频率很高,但它们的创建并不是通过 new 关键字来产生。通常 new 出来的对象都是保存在堆内存中的,以此方式创建小而简单的变量往往是不划算的。所以对于这些基本类型的创建方法,Java 使用了和 C/C++ 一样的策略。也就是说,不是使用 new 创建变量,而是使用一个“自动”变量。 这个变量直接存储"值",并置于栈内存中,因此更加高效。
Java 确定了每种基本类型的内存占用大小。 这些大小不会像其他一些语言那样随着机器环境的变化而变化。这种不变性也是 Java 更具可移植性的一个原因。
基本类型 | 大小 | 取值范围 | 包装类型 | 初始值 |
---|---|---|---|---|
boolean | — | true/false | Boolean | false |
char | 16 bits | Unicode 0~ Unicode 216 -1 | Character | \u0000 (null) |
byte | 8 bits | -128 ~ +127 | Byte | (byte) 0 |
short | 16 bits | - 215 ~ + 215 -1 | Short | (short) 0 |
int | 32 bits | - 231~ + 231 -1 | Integer | 0 |
long | 64 bits | - 263 ~ + 263 -1 | Long | 0L |
float | 32 bits | IEEE754 ~ IEEE754 | Float | 0.0f |
double | 64 bits | IEEE754 ~ IEEE754 | Double | 0.0d |
数组是一种引用数据类型,数组引用变量只是一个引用,数组元素和数组变量在内存里是分开存放的,数组元素被存储在堆内存中。数组引用变量是一个引用类型的变量,被存储在栈内存中。只有当该引用指向有效内存后,才可通过该数组变量来访问数组元素。也就是说,数组引用变量是访问堆内存中数组元素的唯一方式。程序中只能通过str[index]的形式实现:
String[] str=new String[]{"a","b","c"};
String a=str[0];//Output:a
在 Java 中有两种类型的数据可用于高精度的计算。它们是 BigInteger 和 BigDecimal。尽管它们大致可以划归为“包装类型”,但是它们并没有对应的基本类型。由于涉及到的计算量更多,所以运算速度会慢一些。诚然,我们牺牲了速度,但换来了精度。下面列举了简单的使用方式,如果需要进一步学习可自行查阅:
BigDecimal money = new BigDecimal("9.9");
BigDecimal moreMoney = new BigDecimal("9.8");
BigDecimal add = money.add(moreMoney);//加:19.7
BigDecimal subtract = money.subtract(moreMoney);////减:0.1
BigDecimal multiply = money.multiply(moreMoney);////乘:97.02
BigDecimal divide = money.divide(money).setScale(1);////除:1.0,setScale保留1位小数
Java的编写过程中我们需要对一些程序进行注释,除了自己方便阅读,更为别人更好理解自己的程序,所以我们需要进行一些注释,可以是编程思路或者是程序的作用,总而言之就是方便自己他人更好的阅读。
/**
* 国家类
*/
public class State {
}
//国家名称
private String stateName;
/*
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
*/
良好的编程习惯可以有效的提高代码的阅读性和可维护性,编程规范参考《阿里巴巴Java开发手册》。
反例: _ name / name$ / name_ /$name
反例:DaZhePromotion [打折] / getPingfenByName
正例:JavaServerlessPlatform / UserDO / XmlService / TcpUdpDeal / TaPromotion
正例:localValue / getHttpMessage() / inputUserId
正例:MAX_STOCK_COUNT / CACHE_EXPIRED_TIME
正例:com.alibaba.ai.util
在面试过程中经常会被问道, 为什么String不可变?那么到底什么是不可变的对象呢? 可以这样认为:如果一个对象,在它创建完成之后,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变,那么这个对象就是不可变的。
public static void main(String[] args) {
String str = "hello";
System.out.println(str);//Output:hello
str = str + "world";
System.out.println(str);//Output:helloworld
}
从最后的打印结果来看 str 由 hello 变为了 hellworld。 不是说String是不可变的吗? 其实这里存在一个误区: str 只是一个String对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。引用只是一个4字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。 也就是说,str 只是一个引用,它指向了一个具体的对象,当 str = “hello”; 这句代码执行过之后,又创建了一个新的对象str + “world”, 而引用str重新指向了这个新的对象,原来的对象“hello”还在内存中存在,并没有改变。内存结构如下图所示:
在Java中不可能直接操作对象本身,所有的对象都由一个引用指向,必须通过这个引用才能访问对象本身,包括获取成员变量的值,改变对象的成员变量,调用对象的方法等。
查看String 的源码可以看到这个类下面提供的两个变量:value(封装的数组)和hash(哈希值缓存)。
value变量使用final修饰的,一旦初始化了,就不能被改变。String最终会将内容传到value引用中,指向一个数组对象。如图所示:
在String中,调用方法substring, replace, replaceAll, toLowerCase等,可以改变值。其实这些方法内部创建了一个新的String对象。
java到底是值传递还是引用传递至今为止还是有很多程序员不太清楚他们的区别,首先我们得知道什么是值传递,什么是引用传递。
值传递:调用函数时将基本类型实参复制一份传递到函数形参中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
public static void main(String[] args) {
int a = 10;//实参
Value value = new Value();
value.test(a);
System.out.println(a);//Output:10
}
public void test(int b){//形参
b = 99;
}
a作为参数传递给test()方法时,是将内存空间的所指向值传给了方法中b变量,而这个变量也在内存中分配了一个新的存储空间,所以test()方法中所有的操作都只是针对b变量有效,与main()方法中的a变量没有关系了。
引用传递:调用函数时将对象参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
public static void main(String[] args) {
Value value = new Value();
Test test = new Test();
test.setSum(10);
value.test(test);
System.out.println(test.getSum());//Output:99
}
public void test(Test param){
param.setSum(99);
}
前面有说过数据的存储创建对象的时候会将对象的引用test存放栈内存当中,new Test()存放到堆内存中。调用test()方法的时候会将test引用的内存地址传给param,在test()方法中操作都是针对param这个引用与test引用没有关系了,只不过他们的引用地址相同才改变对象属性。
看看下面这段代码为什么String作为对象他的参数没有改变呢?
public static void main(String[] args) {
String str = "hello";
Value value = new Value();
value.test(str);
System.out.println(str);//Output:hello
}
public void test(String param){
param= param+ "world";
}
就如前面所说的String是不可变的是一样的道理,调用test()方法的时候会从新创建一个新的对象引用,变更后重新指向param,从而不影响main()方法中的str。
java中方法参数传递方式都是值传递。关键是看这个值是什么,如果参数是基本类型,传递的是基本类型的就是复制具体值。如果参数是引用类型,传递的是该引用的对象的内存地址。
String str = new String(“abc”)这是一道非常经典的面试题,让很多初学者摸不着头脑,我们通过下面的图片讲解创建了几个对象:
前面我们说过栈内存里面存放的是基本类型和对象的引用,当String str="abc"的时候abc属于常量放在常量池中。堆中存放的是对象的实例。
JVM为了提高性能和减少内存开销,会对实例化字符串常量进行一些优化【即相同字符串无需重新生成,支持数据共享】。所以在当前问题中,系统会先检测常量池中是否含有“abc”这个字符串对象,如果有,就只在常量池中创建一个对象;如果没有,则在常量池中创建一个“abc”字符串对象和堆中创建一个实例对象;
JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
这个问题也算是很经典的一道面试题了,新手程序员都要了解的原理,下面详细介绍一下他们的区别。
对于基本数据类型(byte,short,char,int,float,double,long,boolean)是没有equals()方法作比较的,== 本质上比较是内存地址,如果相等,则说明这两个引用实际是指向同一个对象地址的。前面我们说过基本数据类型的值(包括String常量),都是存放在常量池中,每当声明一个常量的时候Jvm会在常量池查找有没有相同的值,如果有赋给当前引用即原来那个引用和现在这个引用指向了同一对象。
通过上面的讲解,再看下面的代码就很清楚不同的判断应该返回什么样的结果,在以后的工作中能避开这些错误。
public static void main(String[] args) {
String a = new String("123");
String b = new String("123");
String c = "123";
String d = "123";
//Output:false 因为a和b都创建了一个新的内存空间所以等于false
System.out.println(a == b);
//Output:false 因为b和c都存放在不同的内存空间一个在堆一个在常量池.
System.out.println(b == c);
//Output:true 因为c和d都相同所以存放在常量池只会存在一个,地址相同
System.out.println(c == d);
int i1 = 1;
int i2 = 1;
//Output:true i1和i2也一样是存放在常量池中,同上。
System.out.println(i1 == i2);
}
大家常说equals()方法是比较值,但是这不全面。
public static void main(String[] args) {
String a = new String("123");
String b = new String("123");
System.out.println(a.equals(b));//Output: true
}
由于所有的类都是继承自java.lang.Object类的,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,我们可以看到Object类中的equals()方法还是通过 == 判断
竟然我们都知道所有的类都直接或间接地继承自java.lang.Object类,因此我们可以通过重写equals()方法来实现我们自己想要的比较方法。
以String为例,可以看到在Object类的基础上增加了一些新的处理方法。只要业务允许,我们也可以写出属于自已的一套equals()方法。