java中,一个类有三个变量对应三种方法,怎么vb定义变量对象

(转) Android中数据储存的5种方法_C++学习撮要之一:类和对象_Java中equals跟==的区别祥解__脚本百事通
稍等,加载中……
^_^请注意,有可能下面的2篇文章才是您想要的内容:
(转) Android中数据储存的5种方法
C++学习撮要之一:类和对象
Java中equals跟==的区别祥解
(转) Android中数据储存的5种方法
(转) Android中数据存储的5种方法原址:http://apps./share/detail/
Android中数据存储的5种方法
简介:这是Android中数据存储的5中方法的详细页面,介绍了和手机软件,Android Android中数据存储的5中方法有关的知识,
Android数据存储 Android提供了5种方式存储数据: --使用SharedPreferences存储数据; --文件存储数据; --SQLite数据库存储数据; --使用ContentProvider存储数据; --网络存储数据;
先说下,Preference,File, DataBase这三种方式分别对应的目录是/data/data/Package Name/Shared_Pref, /data/data/Package Name/files, /data/data/Package Name/database 。
在Android中通常使用File存储方式是用Context.openFileOutput(String fileName, int mode)和Context.openFileInput(String fileName)。 Context.openFileOutput(String fileName, int mode)生成的文件自动存储在/data/data/Package Name/files目录下,其全路径是/data/data/Package Name/files/fileName 。注意下,这里的参数fileName不可以包含路径分割符(如"/")。 通常来说,这种方式生成的文件只能在这个apk内访问。但这个结论是指使用Context.openFileInput(String fileName)的方式。使用这种方式,每个apk只可以访问自己的/data/data/Package Name/files目录下的文件,原因很简单,参数fileName中不可以包含路径分割符,Android会自动在/data/data/Package Name/files目录下寻找文件名为fileName的文件。
一:使用SharedPreferences存储数据
首先说明SharedPreferences存储方式,它是Android提供的用来存储一些简单配置信息的一种机制,例如:登录用户的用户名与密码。其采用了Map数据结构来存储数据,以键值的方式存储,可以简单的读取与写入,具体实例如下: void ReadSharedPreferences(){ String strName,strP SharedPreferences
user = getSharedPreferences(“user_info”,0); strName = user.getString(“NAME”,””); strPassword = user getString(“PASSWORD”,””); } void WriteSharedPreferences(String strName,String strPassword){ SharedPreferences
user = getSharedPreferences(“user_info”,0); uer.edit(); user.putString(“NAME”, strName); user.putString(“PASSWORD” ,strPassword); <mit(); } 数据读取与写入的方法都非常简单,只是在写入的时候有些区别:先调用edit()使其处于编辑状态,然后才能修改数据,最后使用commit()提交修改的数据。实际上SharedPreferences是采用了XML格式将数据存储到设备中,在DDMS中的File Explorer中的/data/data/&package name&/shares_prefs下。以上面的数据存储结果为例,打开后可以看到一个user_info.xml的文件,打开后可以看到: &?xml version=”1.0″ encoding=”UTF-8″?& &map& &string name=”NAME”&moandroid&/string& &string name=” PASSWORD”&SharedPreferences&/string& &/map& 使用SharedPreferences是有些限制的:只能在同一个包内使用,不能在不同的包之间使用。
二:文件存储数据
文件存储方式是一种较常用的方法,在Android中读取/写入文件的方法,与Java中实现I/O的程序是完全一样的,提供了openFileInput()和openFileOutput()方法来读取设备上的文件。FilterInputStream, FilterOutputStream等可以到Java io package说明中去详细学习,不再此详细说明,具体实例如下: String fn = “moandroid.log”; FileInputStream fis = openFileInput(fn); FileOutputStream fos = openFileOutput(fn,Context.MODE_PRIVATE); 除此之外,Android还提供了其他函数来操作文件,详细说明请阅读Android SDK。
三:网络存储数据
网络存储方式,需要与Android 网络数据包打交道,关于Android 网络数据包的详细说明,请阅读Android SDK引用了Java SDK的哪些package?。
四:ContentProvider
1、ContentProvider简介
当应用继承ContentProvider类,并重写该类用于提供数据和存储数据的方法,就可以向其他应用共享其数据。虽然使用其他方法也可以对外共享数据,但数据访问方式会因数据存储的方式而不同,如:采用文件方式对外共享数据,需要进行文件操作读写数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读写数据。而使用ContentProvider共享数据的好处是统一了数据访问方式。?
2、Uri类简介
Uri代表了要操作的数据,Uri主要包含了两部分信息:1.需要操作的ContentProvider ,2.对ContentProvider中的什么数据进行操作,一个Uri由以下几部分组成: 1.scheme:ContentProvider(内容提供者)的scheme已经由Android所规定为:content://。
2.主机名(或Authority):用于唯一标识这个ContentProvider,外部调用者可以根据这个标识来找到它。
3.路径(path):可以用来表示我们要操作的数据,路径的构建应根据业务而定,如下: ? 要操作contact表中id为10的记录,可以构建这样的路径:/contact/10 ? 要操作contact表中id为10的记录的name字段, contact/10/name ? 要操作contact表中的所有记录,可以构建这样的路径:/contact? 要操作的数据不一定来自数据库,也可以是文件等他存储方式,如下: 要操作xml文件中contact节点下的name节点,可以构建这样的路径:/contact/name 如果要把一个字符串转换成Uri,可以使用Uri类中的parse()方法,如下: Uri uri = Uri.parse("content://com.changcheng.provider.contactprovider/contact") 3、UriMatcher、ContentUrist和ContentResolver简介
因为Uri代表了要操作的数据,所以我们很经常需要解析Uri,并从Uri中获取数据。Android系统提供了两个用于操作Uri的工具类,分别为UriMatcher 和ContentUris 。掌握它们的使用,会便于我们的开发工作。 ? UriMatcher:用于匹配Uri,它的用法如下:
1.首先把你需要匹配Uri路径全部给注册上,如下: //常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码(-1)。 UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); //如果match()方法匹配content://com.changcheng.sqlite.provider.contactprovider/contact路径,返回匹配码为1 uriMatcher.addURI(“com.changcheng.sqlite.provider.contactprovider”, “contact”, 1);//添加需要匹配uri,如果匹配就会返回匹配码 //如果match()方法匹配 content://com.changcheng.sqlite.provider.contactprovider/contact/230路径,返回匹配码为2 uriMatcher.addURI(“com.changcheng.sqlite.provider.contactprovider”, “contact/#”, 2);//#号为通配符 2.注册完需要匹配的Uri后,就可以使用uriMatcher.match(uri)方法对输入的Uri进行匹配,如果匹配就返回匹配码,匹配码是调用addURI()方法传入的第三个参数,假设匹配content://com.changcheng.sqlite.provider.contactprovider/contact路径,返回的匹配码为1。 ? ContentUris:用于获取Uri路径后面的ID部分,它有两个比较实用的方法: ? withAppendedId(uri, id)用于为路径加上ID部分 ? parseId(uri)方法用于从路径中获取ID部分 ? ContentResolver:当外部应用需要对ContentProvider中的数据进行添加、删除、修改和查询操作时,可以使用ContentResolver 类来完成,要获取ContentResolver 对象,可以使用Activity提供的getContentResolver()方法。 ContentResolver使用insert、delete、update、query方法,来操作数据。
五:总结说明
以上5中存储方式,在以后的开发过程中,根据设计目标、性能需求、空间需求等找到合适的数据存储方式。Android 中的数据存储都是私有的,其他应用程序都是无法访问的,除非通过ContentResolver获取其他程序共享的数据。采用文件方式对外共享数据,需要进行文件操作读写数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读写数据。而使用ContentProvider共享数据的好处是统一了数据访问方式。
C++学习撮要之一:类和对象
C++学习摘要之一:类和对象第一章 类和对象
  类和对象是面向对象程序设计的两个最基本、最重要的概念。所谓对象就是客观事务在计算机中的抽象描述;而所谓类则是对具有相似属性和行为的一组对象的统一描述。从程序设计语言的角度来说,类是一种数据类型,而对象是具有这种类型的变量。
  1.类的定义
  类定义的一般形式为:
  class 类名
  private:
  数据成员或成员函数
  protected:
  数据成员或成员函数
  public:
  数据成员或成员函数
  &各成员函数的实现代码&
 其中,class是定义类的关键字。类名是一个有效的标志符,且一般首字母大写。大括号括起来的部分是类说明部分,它声明了类的所有成员(包括数据成员
和函数成员),这些成员从访问权限上分成三类,即私有(private)、公有(public)和保护(protected),其中默认权限为
  类的private部分说明的成员,在类之外是不能存取的,只有类中的成员函数才能存取private的数据成员和成
员函数。类的public部分说明的成员,可被程序中的任何函数或语句存取,public成员多为成员函数,用来提供一个与外界的接口,外界只有通过这个
接口才可以实现对private成员的存取。类的protected部分说明的成员,不能在类之外存取,只有类的成员函数及其子类(派生类)可以存取
protected的成员。
  2.成员函数的定义
  类中的成员函数可以在两处定义:一是将成员函数的定义直接写在类中,一般适合于成员函数规模较小的情况;二是在类的定义体中只写出成员函数的原型说明,而成员函数的定义写在类的定义之外,这种情况比较适合于成员函数体较大的情况,其定义格式为:
  返回值类型 类名::成员函数名(参数说明)
  函数体;
  此处的“::”符号称为作用域运算法(名空间分隔符),它是用来指明哪个函数属于哪个类或哪个数据属于哪个类。
  需要说明的是,成员函数既可以是有参函数,也可以像普通函数一样,给形参指定默认值。
  3.对象及指向对象的指针
  对象是类的实例,定义对象之前一定要说明该对象的类。定义对象的一般格式为:
  类名 对象名表;
  其中,对象名表中可以有一个或多个对象名,多个对象名之间用逗号分隔。
  另外,也可以定义指向类类型的指针,其定义格式为:
  类名 *指针变量名;
  4.访问对象的成员
  访问对象的成员有以下两种格式:
  (1) 对象名。成员名
  (2) 指针变量名-&成员名
  第一种格式是利用对象和运算符“。”访问成员;而第二种格式是采用指向对象的指针和运算符“-&”来访问成员,并且第二种格式中的指针变量必须已指向某个对象。
  5.类成员指针
  在C++语言中,除了可以定义指针对象外,还可以定义类成员指针。
  类数据成员指针就是程序中定义的用于指向类中数据成员的指针变量,借助该指针变量可以访问它所指向的类中的数据成员(该数据成员必须具有public访问权限),但该指针变量不是类的成员,它只是程序中的一个指针变量而已。
  类数据成员指针的定义格式如下:
  类型 类名::*指针变量名;
  此处的“类型”要与类中数据成员的类型保持一致。
  要使已经定义的类数据成员的指针变量指向类中某个数据成员时,可以通过以下语句:
  类数据成员指针变量名=&类名::类数据成员变量名;
  当类数据成员指针变量已经指向类中某个数据成员时,可以通过以下语句访问类中数据成员:
对象名。*类数据成员指针变量名;
  6.this指针
  在每一个类成员函数的形参表中都有一个隐含的指针变量this,该指针变量的类型就是成员函数所属类的类型。当程序中调用成员函数时,this指针变量被自动初始化为发出函数调用的对象的地址。
  尽管我们在定义成员函数时没有看到this指针变量,也没有定义this指针变量,但是在成员函数的函数体内我们可以使用this指针变量,因为该指针变量使系统隐含给出的,我们不需要也不能在成员函数的形参表中对this指针变量进行显示说明。
  类中成员函数有了隐含的指针变量this后,就可以保证用不同的对象调用成员函数是对不同对象的操作。
  7.对象赋值语句
  对于同一个类生成的两个对象,可以进行赋值,其功能是将一个对象的数据成员赋值到另一个对象中去,赋值语句的左右两边各是一个对象名。
  如,已知一个类Example,则:
  Example obj1,obj2;
  obj2=obj1;//将对象obj1的数据成员赋给对象obj2
  8.对象的作用域和生存期
  在不同的位置以不同的方式定义对象时,其作用域和生存期是不同的,其原理与普通变量相同。可分为局部对象(不包括局部静态对象)、静态对象(局部静态对象和全局静态对象)、全局对象等。
  9.堆对象
  所谓堆对象是指在程序运行过程中,根据需要随时可以建立和删除的对象。堆对象被创建在内存中一些空闲的存储单元中,这些存储单元被称为堆。堆可以被创建的对象占有,可以通过删除堆对象而获得释放。
  需要利用new运算符创建堆对象,利用delete运算符删除堆对象。堆对象的生存期是整个程序的生命期。如:
  Example *p;//p为指向类Example对象的一个指针
  p=new Example(); //使用new给p分配内存空间
  delete p; //使用delete释放p所指向的空间
  10.对象数组
  当一个数组的类型为类类型时,该数组中的每个元素都是该类中的一个对象,则这种数组就是对象数组。对象数组的定义格式为:
  类名 数组名 [数组大小];
  Example array[10];
  表明array数组是一个一维对象数组,该数组有10个元素,从array[0]到array[9],其中每个元素都是类Example的对象。
Java中equals跟==的区别祥解
Java中equals和==的区别祥解对象比较 ==比较的是2个对象的地址,而equals比较的是2个对象的内容。 显然,当equals为true时,==不一定为true; 一、String中的equals和== 1、 public class TestString {
public static void main(String[] args) {
String s1 = "Monday";
String s2 = "Monday";
} } 上面这段程序中,到底有几个对象呢? 来检测一下吧,稍微改动一下程序 public class TestString {
public static void main(String[] args) {
String s1 = "Monday";
String s2 = "Monday";
if (s1 == s2)
System.out.println("s1 == s2");
System.out.println("s1 != s2");
} } 编译并运行程序,输出:s1 == s2 说明:s1 与 s2 引用同一个 String 对象 -- "Monday"! 2. 再稍微改动一下程序,会有更奇怪的发现: public class TestString {
public static void main(String[] args) {
String s1 = "Monday";
String s2 = new String("Monday");
if (s1 == s2)
System.out.println("s1 == s2");
System.out.println("s1 != s2");
if (s1.equals(s2))
System.out.println("s1 equals s2");
System.out.println("s1 not equals s2");
} } 我们将 s2 用 new 操作符创建 程序输出: s1 != s2 s1 equals s2 说明:s1 s2分别引用了两个"Monday"String对象 3. 字符串缓冲池 原来,程序在运行的时候会创建一个字符串缓冲池 当使用 s2 = "Monday" 这样的表达是创建字符串的时候,程序首先会 在这个String缓冲池中寻找相同值的对象,在第一个程序中,s1先被 放到了池中,所以在s2被创建的时候,程序找到了具有相同值的 s1 将 s2 引用 s1 所引用的对象"Monday"第二段程序中,使用了 new 操作符,他明白的告诉程序: "我要一个新的!不要旧的!"于是一个新的"Monday"Sting对象被创 建在内存中。他们的值相同,但是位置不同,一个在池中游泳 一个在岸边休息。哎呀,真是资源浪费,明明是一样的非要分开做什么呢? 4. 再次更改程序: public class TestString {
public static void main(String[] args) {
String s1 = "Monday";
String s2 = new String("Monday");
s2 = s2.intern();
if (s1 == s2)
System.out.println("s1 == s2");
System.out.println("s1 != s2");
if (s1.equals(s2))
System.out.println("s1 equals s2");
System.out.println("s1 not equals s2");
} } 这次加入:s2 = s2.intern(); 程序输出: s1 == s2 s1 equals s2 原来,(java.lang.String的intern()方法 "abc".intern()方法的返回值还是字符串"abc",表面上看起来好像这个方法没什么用处。但实际上,它做了个小动作: 检查字符串池里是否存在"abc"这么一个字符串,如果存在,就返回池里的字符串;如果不存在,该方法会把"abc"添加到字符串池中,然后再返回它的引用。 ) 更好的办法: 把所有的String都intern()到缓冲池去吧 最好在用到new的时候就进行这个操作 String s2 = new String("Monday").intern(); 然后就可以用==比较两个字符串的值了 二、简单数据类型和封装类中的equals和== Java为每一个简单数据类型提供了一个封装类,每个基本数据类型可以封装成对象类型。 除int(Integer)和char(Character),其余类型首字母大写即成封装类类型名。double (Double), float(Float),long(Long), short(Short),byte(Byte),boolean(Boolean).
以int和Integer为例说明 Java中int和Integer区别如下: 1.int是基本的数据类型,默认值可以为0; 2.Integer是int的封装类,默认值为null; 3.int和Integer都可以表示某一个数值; 4.int和Integer不能够互用,因为他们两种不同的数据类型; int a1=1; int a2=1; Integer b1 =new Integer (1); Integer b2 =new Integer (1); ------------------------------ a1==a2 这个是成立的,很简单,都知道 a1==b1 这个是不成立的.表达式的值为 false ,它们是不同的数据类型 b1==b2 这个也是不成立的.表达式的值为 false,虽然是相同的数据类型,但是它们是两个对象,==比较的是2个对象的地址,它们的地址是不相等的,内容相等都是1; b1.equals(b2)==true 这个是成立的,表达式的值为 true. 相同数据类型,两个对象,地址不同,内容相同, quals比较的是2个对象的内容,所以成立。 (a.equals(b),因为equals比较的是两个对象,所以a,b都不能为基本数据类型,否则会出编译错误。) 同理,其它的封装类和基本类型也是这样的. java中equals和==的区别 ==比较的是2个对象的地址,而equals比较的是2个对象的内容。 三、其他类怎么使用equals和== API里的类大部分都重写了equals方法,没有重写的一般是自己写的类, 如果是你自己定义的一个类,比较自定义类用equals和==是一样的,都是比较句柄地址, 因为自定义的类是继承于object,而object中的equals就是用==来实现的,你可以看源码。 四、java里equals和hashCode之间什么关系 只是为了维护 hashCode 方法的常规协定,才要求用equals比较的两个对象的hashCode相同. equals()和hashCode()都来自java.lang.Object.你当然可以重写. 比如a.equals(b).仅当a的内存地址相等时,才返回true.当然如String等类已经对这个方法进行了重写,比较的就不再是内存地址了. hashCode()的值也是与内存地址相关的.所以仅当内存地址相等时,hashCode才相等. 同样很多类也重写了这个方法,还是以String为例: public int hashCode() { int h = if (h == 0) {
char val[] =
for (int i = 0; i & i++) {
h = 31*h + val[off++];
} 就不在与内存地址相关了.这样做是为了保证用equals比较返回为true的两个对象,他们的hashCode是相同的. 所以一般重写equals的时候都会重写hashCode(). 当然,这个相当于一个约定,一个协议.你不这么做并不会错. 五、hashCode 在一般的应用中你不需要了解hashcode的用法,但当你用到hashmap,hashset等集合类时要注意下hashcode。 你想通过一个object的key来拿hashmap的value,hashmap的工作方法是, 通过你传入的object的hashcode在内存中找地址, 当找到这个地址后再通过equals方法来比较这个地址中的内容是否和你原来放进去的一样,一样就取出value。 所以这里要匹配2部分,hashcode和equals 但假如说你new一个object作为key去拿value是永远得不到结果的, 因为每次new一个object,这个object的hashcode是永远不同的,所以我们要重写hashcode, 你可以令你的hashcode是object中的一个恒量,这样永远可以通过你的object的hashcode来找到key的地址, 然后你要重写你的equals方法,使内存中的内容也相等。。。
如果您想提高自己的技术水平,欢迎加入本站官方1号QQ群:&&,&&2号QQ群:,在群里结识技术精英和交流技术^_^
本站联系邮箱:用java定义一个类,成员是2个double型变量,成员方法有setter和getter方法,计算并返回变量的加减乘除运算结果
用java定义一个类,成员是2个double型变量,成员方法有setter和getter方法,计算并返回变量的加减乘除运算结果
不区分大小写匿名
public class ABC&{&&&&public static void main(String[] args)&{& ABC obj1 = new ABC();& obj1.setA(5.8D);& obj1.setB(2.4D);&&& System.out.println("5.8 + 2.4 = " + obj1.add());& System.out.println("5.8 - 2.4 = " + obj1.sub());& System.out.println("5.8 * 2.4 = " + obj1.mul());& System.out.println("5.8 / 2.4 = " + obj1.div());&}& public double add(){& return a +&}& public double sub(){& return a -&}&public double mul(){& return a *&}&public double div(){& return (double)a /&}&public double getA()&{&&}&public void setA(double a)&{& this.a =&}&public double getB()&{&&}&public void setB(double b)&{& this.b =&}}已经在java中测试过了,没问题。
嗯,谢谢啊
相关知识等待您来回答
编程领域专家页面导航:
→ 正文内容 继承、多态、重载和重写
Java中继承、多态、重载和重写介绍
这篇文章主要介绍了Java中继承、多态、重载和重写介绍,需要的朋友可以参考下
什么是多态?它的实现机制是什么呢?重载和重写的区别在那里?这就是这一次我们要回顾的四个十分重要的概念:继承、多态、重载和重写。
继承(inheritance)
简单的说,继承就是在一个现有类型的基础上,通过增加新的方法或者重定义已有方法(下面会讲到,这种方式叫重写)的方式,产生一个新的类型。继承是面向对象的三个基本特征--封装、继承、多态的其中之一,我们在使用JAVA时编写的每一个类都是在继承,因为在JAVA语言中,java.lang.Object类是所有类最根本的基类(或者叫父类、超类),如果我们新定义的一个类没有明确地指定继承自哪个基类,那么JAVA就会默认为它是继承自Object类的。
我们可以把JAVA中的类分为以下三种:
类:使用class定义且不含有抽象方法的类。
抽象类:使用abstract class定义的类,它可以含有,也可以不含有抽象方法。
接口:使用interface定义的类。
在这三种类型之间存在下面的继承规律:
类可以继承(extends)类,可以继承(extends)抽象类,可以继承(implements)接口。
抽象类可以继承(extends)类,可以继承(extends)抽象类,可以继承(implements)接口。
接口只能继承(extends)接口。
请注意上面三条规律中每种继承情况下使用的不同的关键字extends和implements,它们是不可以随意替换的。大家知道,一个普通类继承一个接口后,必须实现这个接口中定义的所有方法,否则就只能被定义为抽象类。我在这里之所以没有对implements关键字使用“实现”这种说法是因为从概念上来说它也是表示一种继承关系,而且对于抽象类implements接口的情况下,它并不是一定要实现这个接口定义的任何方法,因此使用继承的说法更为合理一些。
以上三条规律同时遵守下面这些约束:
类和抽象类都只能最多继承一个类,或者最多继承一个抽象类,并且这两种情况是互斥的,也就是说它们要么继承一个类,要么继承一个抽象类。
类、抽象类和接口在继承接口时,不受数量的约束,理论上可以继承无限多个接口。当然,对于类来说,它必须实现它所继承的所有接口中定义的全部方法。
抽象类继承抽象类,或者实现接口时,可以部分、全部或者完全不实现父类抽象类的抽象(abstract)方法,或者父类接口中定义的接口。
类继承抽象类,或者实现接口时,必须全部实现父类抽象类的全部抽象(abstract)方法,或者父类接口中定义的全部接口。
继承给我们的编程带来的好处就是对原有类的复用(重用)。就像模块的复用一样,类的复用可以提高我们的开发效率,实际上,模块的复用是大量类的复用叠加后的效果。除了继承之外,我们还可以使用组合的方式来复用类。所谓组合就是把原有类定义为新类的一个属性,通过在新类中调用原有类的方法来实现复用。如果新定义的类型与原有类型之间不存在被包含的关系,也就是说,从抽象概念上来讲,新定义类型所代表的事物并不是原有类型所代表事物的一种,比如黄种人是人类的一种,它们之间存在包含与被包含的关系,那么这时组合就是实现复用更好的选择。下面这个例子就是组合方式的一个简单示例:
public class Sub {
private Parent p = new Parent();
public void doSomething() {
// 复用Parent类的方法
p.method();
// other code
class Parent {
public void method() {
// do something here
&当然,为了使代码更加有效,我们也可以在需要使用到原有类型(比如Parent p)时,才对它进行初始化。
使用继承和组合复用原有的类,都是一种增量式的开发模式,这种方式带来的好处是不需要修改原有的代码,因此不会给原有代码带来新的BUG,也不用因为对原有代码的修改而重新进行测试,这对我们的开发显然是有益的。因此,如果我们是在维护或者改造一个原有的系统或模块,尤其是对它们的了解不是很透彻的时候,就可以选择增量开发的模式,这不仅可以大大提高我们的开发效率,也可以规避由于对原有代码的修改而带来的风险。
多态(Polymorphism)
多态是又一个重要的基本概念,上面说到了,它是面向对象的三个基本特征之一。究竟什么是多态呢?我们先看看下面的例子,来帮助理解:
//汽车接口
interface Car {
// 汽车名称
String getName();
// 获得汽车售价
int getPrice();
class BMW implements Car {
public String getName() {
return "BMW";
public int getPrice() {
return 300000;
class CheryQQ implements Car {
public String getName() {
return "CheryQQ";
public int getPrice() {
return 20000;
// 汽车出售店
public class CarShop {
// 售车收入
private int money = 0;
// 卖出一部车
public void sellCar(Car car) {
System.out.println("车型:" + car.getName() + " 单价:" + car.getPrice());
// 增加卖出车售价的收入
money += car.getPrice();
// 售车总收入
public int getMoney() {
public static void main(String[] args) {
CarShop aShop = new CarShop();
// 卖出一辆宝马
aShop.sellCar(new BMW());
// 卖出一辆奇瑞QQ
aShop.sellCar(new CheryQQ());
System.out.println("总收入:" + aShop.getMoney());
运行结果:
车型:BMW& 单价:300000
车型:CheryQQ& 单价:20000
总收入:320000
继承是多态得以实现的基础。从字面上理解,多态就是一种类型(都是Car类型)表现出多种状态(宝马汽车的名称是BMW,售价是300000;奇瑞汽车的名称是CheryQQ,售价是2000)。将一个方法调用同这个方法所属的主体(也就是对象或类)关联起来叫做绑定,分前期绑定和后期绑定两种。下面解释一下它们的定义:
前期绑定:在程序运行之前进行绑定,由编译器和连接程序实现,又叫做静态绑定。比如static方法和final方法,注意,这里也包括private方法,因为它是隐式final的。
后期绑定:在运行时根据对象的类型进行绑定,由方法调用机制实现,因此又叫做动态绑定,或者运行时绑定。除了前期绑定外的所有方法都属于后期绑定。
多态就是在后期绑定这种机制上实现的。多态给我们带来的好处是消除了类之间的耦合关系,使程序更容易扩展。比如在上例中,新增加一种类型汽车的销售,只需要让新定义的类继承Car类并实现它的所有方法,而无需对原有代码做任何修改,CarShop类的sellCar(Car car)方法就可以处理新的车型了。新增代码如下:
// 桑塔纳汽车
class Santana implements Car {
public String getName() {
return "Santana";
public int getPrice() {
return 80000;
重载(overloading)和重写(overriding)
重载和重写都是针对方法的概念,在弄清楚这两个概念之前,我们先来了解一下什么叫方法的型构(英文名是signature,有的译作“签名”,虽然它被使用的较为广泛,但是这个翻译不准确的)。型构就是指方法的组成结构,具体包括方法的名称和参数,涵盖参数的数量、类型以及出现的顺序,但是不包括方法的返回值类型,访问权限修饰符,以及abstract、static、final等修饰符。比如下面两个就是具有相同型构的方法:
public void method(int i, String s) {
// do something
public String method(int i, String s) {
// do something
而这两个就是具有不同型构的方法:
public void method(int i, String s) {
// do something
public void method(String s, int i) {
// do something
了解完型构的概念后我们再来看看重载和重写,请看它们的定义:
重写,英文名是overriding,是指在继承情况下,子类中定义了与其基类中方法具有相同型构的新方法,就叫做子类把基类的方法重写了。这是实现多态必须的步骤。
重载,英文名是overloading,是指在同一个类中定义了一个以上具有相同名称,但是型构不同的方法。在同一个类中,是不允许定义多于一个的具有相同型构的方法的。
我们来考虑一个有趣的问题:构造器可以被重载吗?答案当然是可以的,我们在实际的编程中也经常这么做。实际上构造器也是一个方法,构造器名就是方法名,构造器参数就是方法参数,而它的返回值就是新创建的类的实例。但是构造器却不可以被子类重写,因为子类无法定义与基类具有相同型构的构造器。
重载、覆盖、多态与函数隐藏
经常看到C++的一些初学者对于重载、覆盖、多态与函数隐藏的模糊理解。在这里写一点自己的见解,希望能够C++初学者解惑。
要弄清楚重载、覆盖、多态与函数隐藏之间的复杂且微妙关系之前,我们首先要来回顾一下重载覆盖等基本概念。
首先,我们来看一个非常简单的例子,理解一下什么叫函数隐藏hide。
#include &iostream&
class Base{
void fun() { cout && "Base::fun()" && }
class Derive : public Base{
void fun(int i) { cout && "Derive::fun()" && }
int main()
//下面一句错误,故屏蔽掉
//d.fun();error C2660: 'fun' : function does not take 0 parameters
Derive *pd =new Derive();
//下面一句错误,故屏蔽掉
//pd-&fun();error C2660: 'fun' : function does not take 0 parameters
pd-&fun(1);
/*在不同的非命名空间作用域里的函数不构成重载,子类和父类是不同的两个作用域。
在本例中,两个函数在不同作用域中,故不够成重载,除非这个作用域是命名空间作用域。*/
在这个例子中,函数不是重载overload,也不是覆盖override,而是隐藏hide。
接下来的5个例子具体说明一下什么叫隐藏
#include &iostream&
class Basic{
void fun(){cout && "Base::fun()" &&}//overload
void fun(int i){cout && "Base::fun(int i)" &&}//overload
class Derive :public Basic{
void fun2(){cout && "Derive::fun2()" &&}
int main()
d.fun();//正确,派生类没有与基类同名函数声明,则基类中的所有同名重载函数都会作为候选函数。
d.fun(1);//正确,派生类没有与基类同名函数声明,则基类中的所有同名重载函数都会作为候选函数。
#include &iostream&
class Basic{
void fun(){cout && "Base::fun()" &&}//overload
void fun(int i){cout && "Base::fun(int i)" &&}//overload
class Derive :public Basic{
//新的函数版本,基类所有的重载版本都被屏蔽,在这里,我们称之为函数隐藏hide
//派生类中有基类的同名函数的声明,则基类中的同名函数不会作为候选函数,即使基类有不同的参数表的多个版本的重载函数。
void fun(int i,int j){cout && "Derive::fun(int i,int j)" &&}
void fun2(){cout && "Derive::fun2()" &&}
int main()
d.fun(1,2);
//下面一句错误,故屏蔽掉
//d.fun();error C2660: 'fun' : function does not take 0 parameters
#include &iostream&
class Basic{
void fun(){cout && "Base::fun()" &&}//overload
void fun(int i){cout && "Base::fun(int i)" &&}//overload
class Derive :public Basic{
//覆盖override基类的其中一个函数版本,同样基类所有的重载版本都被隐藏hide
//派生类中有基类的同名函数的声明,则基类中的同名函数不会作为候选函数,即使基类有不同的参数表的多个版本的重载函数。
void fun(){cout && "Derive::fun()" &&}
void fun2(){cout && "Derive::fun2()" &&}
int main()
//下面一句错误,故屏蔽掉
//d.fun(1);error C2660: 'fun' : function does not take 1 parameters
#include &iostream&
class Basic{
void fun(){cout && "Base::fun()" &&}//overload
void fun(int i){cout && "Base::fun(int i)" &&}//overload
class Derive :public Basic{
using Basic::
void fun(){cout && "Derive::fun()" &&}
void fun2(){cout && "Derive::fun2()" &&}
int main()
d.fun();//正确
d.fun(1);//正确
Derive::fun()
Base::fun(int i)
Press any key to continue
#include &iostream&
class Basic{
void fun(){cout && "Base::fun()" &&}//overload
void fun(int i){cout && "Base::fun(int i)" &&}//overload
class Derive :public Basic{
using Basic::
void fun(int i,int j){cout && "Derive::fun(int i,int j)" &&}
void fun2(){cout && "Derive::fun2()" &&}
int main()
d.fun();//正确
d.fun(1);//正确
d.fun(1,2);//正确
Base::fun()
Base::fun(int i)
Derive::fun(int i,int j)
Press any key to continue
好了,我们先来一个小小的总结重载与覆盖两者之间的特征
重载overload的特征:
n&&&&&&&&& 相同的范围(在同一个类中);
n&&&&&&&&& 函数名相同参数不同;
n&&&&&&&&& virtual 关键字可有可无。
覆盖override是指派生类函数覆盖基类函数,覆盖的特征是:
n&&&&&&&&& 不同的范围(分别位于派生类与基类);
n&&&&&&&&& 函数名和参数都相同;
n&&&&&&&&& 基类函数必须有virtual 关键字。(若没有virtual 关键字则称之为隐藏hide)
如果基类有某个函数的多个重载(overload)版本,而你在派生类中重写(override)了基类中的一个或多个函数版本,或是在派生类中重新添加了新的函数版本(函数名相同,参数不同),则所有基类的重载版本都被屏蔽,在这里我们称之为隐藏hide。所以,在一般情况下,你想在派生类中使用新的函数版本又想使用基类的函数版本时,你应该在派生类中重写基类中的所有重载版本。你若是不想重写基类的重载的函数版本,则你应该使用例4或例5方式,显式声明基类名字空间作用域。
事实上,C++编译器认为,相同函数名不同参数的函数之间根本没有什么关系,它们根本就是两个毫不相关的函数。只是C++语言为了模拟现实世界,为了让程序员更直观的思维处理现实世界中的问题,才引入了重载和覆盖的概念。重载是在相同名字空间作用域下,而覆盖则是在不同的名字空间作用域下,比如基类和派生类即为两个不同的名字空间作用域。在继承过程中,若发生派生类与基类函数同名问题时,便会发生基类函数的隐藏。当然,这里讨论的情况是基类函数前面没有virtual 关键字。在有virtual 关键字关键字时的情形我们另做讨论。
继承类重写了基类的某一函数版本,以产生自己功能的接口。此时C++编绎器认为,你现在既然要使用派生类的自己重新改写的接口,那我基类的接口就不提供给你了(当然你可以用显式声明名字空间作用域的方法,见[C++基础]重载、覆盖、多态与函数隐藏(1))。而不会理会你基类的接口是有重载特性的。若是你要在派生类里继续保持重载的特性,那你就自己再给出接口重载的特性吧。所以在派生类里,只要函数名一样,基类的函数版本就会被无情地屏蔽。在编绎器中,屏蔽是通过名字空间作用域实现的。
所以,在派生类中要保持基类的函数重载版本,就应该重写所有基类的重载版本。重载只在当前类中有效,继承会失去函数重载的特性。也就是说,要把基类的重载函数放在继承的派生类里,就必须重写。
这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,具体规则我们也来做一小结:
n&&&&&&&&& 如果派生类的函数与基类的函数同名,但是参数不同。此时,若基类无virtual关键字,基类的函数将被隐藏。(注意别与重载混淆,虽然函数名相同参数不同应称之为重载,但这里不能理解为重载,因为派生类和基类不在同一名字空间作用域内。这里理解为隐藏)
n&&&&&&&&& 如果派生类的函数与基类的函数同名,但是参数不同。此时,若基类有virtual关键字,基类的函数将被隐式继承到派生类的vtable中。此时派生类vtable中的函数指向基类版本的函数地址。同时这个新的函数版本添加到派生类中,作为派生类的重载版本。但在基类指针实现多态调用函数方法时,这个新的派生类函数版本将会被隐藏。
n&&&&&&&&& 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏。(注意别与覆盖混淆,这里理解为隐藏)。
n&&&&&&&&& 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数有virtual关键字。此时,基类的函数不会被“隐藏”。(在这里,你要理解为覆盖哦^_^)。
插曲:基类函数前没有virtual关键字时,我们要重写更为顺口些,在有virtual关键字时,我们叫覆盖更为合理些,戒此,我也希望大家能够更好的理解C++中一些微妙的东西。费话少说,我们举例说明吧。
#include &iostream&
class Base{
virtual void fun() { cout && "Base::fun()" && }//overload
virtual void fun(int i) { cout && "Base::fun(int i)" && }//overload
class Derive : public Base{
void fun() { cout && "Derive::fun()" && }//override
void fun(int i) { cout && "Derive::fun(int i)" && }//override
void fun(int i,int j){ cout&& "Derive::fun(int i,int j)" &&}//overload
int main()
Base *pb = new Derive();
pb-&fun();
pb-&fun(1);
//下面一句错误,故屏蔽掉
//pb-&fun(1,2);virtual函数不能进行overload,error C2661: 'fun' : no overloaded function takes 2 parameters
Derive *pd = new Derive();
pd-&fun();
pd-&fun(1);
pd-&fun(1,2);//overload
Derive::fun()
Derive::fun(int i)
Derive::fun()
Derive::fun(int i)
Derive::fun(int i,int j)
Press any key to continue
#include &iostream&
class Base{
virtual void fun(int i){ cout &&"Base::fun(int i)"&& }
class Derive : public Base{};
int main()
Base *pb = new Derive();
pb-&fun(1);//Base::fun(int i)
#include &iostream&
class Base{
virtual void fun(int i){ cout &&"Base::fun(int i)"&& }
class Derive : public Base{
void fun(double d){ cout &&"Derive::fun(double d)"&& }
int main()
Base *pb = new Derive();
pb-&fun(1);//Base::fun(int i)
pb-&fun((double)0.01);//Base::fun(int i)
#include &iostream&
class Base{
virtual void fun(int i){ cout &&"Base::fun(int i)"&& }
class Derive : public Base{
void fun(int i){ cout &&"Derive::fun(int i)"&& }
int main()
Base *pb = new Derive();
pb-&fun(1);//Derive::fun(int i)
#include &iostream&
class Base{
virtual void fun(int i){ cout &&"Base::fun(int i)"&& }
class Derive : public Base{
void fun(int i){ cout &&"Derive::fun(int i)"&& }
void fun(double d){ cout &&"Derive::fun(double d)"&& }
int main()
Base *pb = new Derive();
pb-&fun(1);//Derive::fun(int i)
pb-&fun((double)0.01);//Derive::fun(int i)
#include &iostream&
class Base{
virtual void fun(int i){ cout &&"Base::fun(int i)"&& }
class Derive : public Base{
void fun(int i){ cout &&"Derive::fun(int i)"&& }
void fun(char c){ cout &&"Derive::fun(char c)"&& }
void fun(double d){ cout &&"Derive::fun(double d)"&& }
int main()
Base *pb = new Derive();
pb-&fun(1);//Derive::fun(int i)
pb-&fun('a');//Derive::fun(int i)
pb-&fun((double)0.01);//Derive::fun(int i)
Derive *pd =new Derive();
pd-&fun(1);//Derive::fun(int i)
//overload
pd-&fun('a');//Derive::fun(char c)
//overload
pd-&fun(0.01);//Derive::fun(double d)
例7-1和例8-1很好理解,我把这两个例子放在这里,是让大家作一个比较摆了,也是为了帮助大家更好的理解:
n&&&&&&&&& 例7-1中,派生类没有覆盖基类的虚函数,此时派生类的vtable中的函数指针指向的地址就是基类的虚函数地址。
n&&&&&&&&& 例8-1中,派生类覆盖了基类的虚函数,此时派生类的vtable中的函数指针指向的地址就是派生类自己的重写的虚函数地址。
在例7-2和8-2看起来有点怪怪,其实,你按照上面的原则对比一下,答案也是明朗的:
n&&&&&&&&& 例7-2中,我们为派生类重载了一个函数版本:void fun(double d)& 其实,这只是一个障眼法。我们具体来分析一下,基类共有几个函数,派生类共有几个函数:
Vtable部分
&void fun(int i)
&指向基类版的虚函数void fun(int i)
&void fun(double d)
我们再来分析一下以下三句代码
Base *pb = new Derive();
pb-&fun(1);//Base::fun(int i)
pb-&fun((double)0.01);//Base::fun(int i)
这第一句是关键,基类指针指向派生类的对象,我们知道这是多态调用;接下来第二句,运行时基类指针根据运行时对象的类型,发现是派生类对象,所以首先到派生类的vtable中去查找派生类的虚函数版本,发现派生类没有覆盖基类的虚函数,派生类的vtable只是作了一个指向基类虚函数地址的一个指向,所以理所当然地去调用基类版本的虚函数。最后一句,程序运行仍然埋头去找派生类的vtable,发现根本没有这个版本的虚函数,只好回头调用自己的仅有一个虚函数。
这里还值得一提的是:如果此时基类有多个虚函数,此时程序编绎时会提示”调用不明确”。示例如下
#include &iostream&
class Base{
virtual void fun(int i){ cout &&"Base::fun(int i)"&& }
virtual void fun(char c){ cout &&"Base::fun(char c)"&& }
class Derive : public Base{
void fun(double d){ cout &&"Derive::fun(double d)"&& }
int main()
Base *pb = new Derive();
pb-&fun(0.01);//error C2668: 'fun' : ambiguous call to overloaded function
好了,我们再来分析一下例8-2。
n&&&&&&&&& 例8-2中,我们也为派生类重载了一个函数版本:void fun(double d)& ,同时覆盖了基类的虚函数,我们再来具体来分析一下,基类共有几个函数,派生类共有几个函数:
Vtable部分
&void fun(int i)
&void fun(int i)
&void fun(double d)
&从表中我们可以看到,派生类的vtable中函数指针指向的是自己的重写的虚函数地址。
我们再来分析一下以下三句代码
Base *pb = new Derive();
pb-&fun(1);//Derive::fun(int i)
pb-&fun((double)0.01);//Derive::fun(int i)
第一句不必多说了,第二句,理所当然调用派生类的虚函数版本,第三句,嘿,感觉又怪怪的,其实呀,C++程序很笨的了,在运行时,埋头闯进派生类的vtable表中,只眼一看,靠,竞然没有想要的版本,真是想不通,基类指针为什么不四处转转再找找呢&#63;呵呵,原来是眼力有限,基类年纪这么老了,想必肯定是老花了,它那双眼睛看得到的仅是自己的非Vtable部分(即静态部分)和自己要管理的Vtable部分,派生类的void fun(double d)那么远,看不到呀!再说了,派生类什么都要管,难道派生类没有自己的一点权力吗&#63;哎,不吵了,各自管自己的吧^_^
唉!你是不是要叹气了,基类指针能进行多态调用,但是始终不能进行派生类的重载调用啊(参考例6)~~~
再来看看例9,
本例的效果同例6,异曲同工。想必你理解了上面的这些例子后,这个也是小Kiss了。
&&&&&&& 重载overload是根据函数的参数列表来选择要调用的函数版本,而多态是根据运行时对象的实际类型来选择要调用的虚virtual函数版本,多态的实现是通过派生类对基类的虚virtual函数进行覆盖override来实现的,若派生类没有对基类的虚virtual函数进行覆盖override的话,则派生类会自动继承基类的虚virtual函数版本,此时无论基类指针指向的对象是基类型还是派生类型,都会调用基类版本的虚virtual函数;如果派生类对基类的虚virtual函数进行覆盖override的话,则会在运行时根据对象的实际类型来选择要调用的虚virtual函数版本,例如基类指针指向的对象类型为派生类型,则会调用派生类的虚virtual函数版本,从而实现多态。
&&&&&&& 使用多态的本意是要我们在基类中声明函数为virtual,并且是要在派生类中覆盖override基类的虚virtual函数版本,注意,此时的函数原型与基类保持一致,即同名同参数类型;如果你在派生类中新添加函数版本,你不能通过基类指针动态调用派生类的新的函数版本,这个新的函数版本只作为派生类的一个重载版本。还是同一句话,重载只有在当前类中有效,不管你是在基类重载的,还是在派生类中重载的,两者互不牵连。如果明白这一点的话,在例6、例9中,我们也会对其的输出结果顺利地理解。
&&&&&&& 重载是静态联编的,多态是动态联编的。进一步解释,重载与指针实际指向的对象类型无关,多态与指针实际指向的对象类型相关。若基类的指针调用派生类的重载版本,C++编绎认为是非法的,C++编绎器只认为基类指针只能调用基类的重载版本,重载只在当前类的名字空间作用域内有效,继承会失去重载的特性,当然,若此时的基类指针调用的是一个虚virtual函数,那么它还会进行动态选择基类的虚virtual函数版本还是派生类的虚virtual函数版本来进行具体的操作,这是通过基类指针实际指向的对象类型来做决定的,所以说重载与指针实际指向的对象类型无关,多态与指针实际指向的对象类型相关。
&&& 最后阐明一点,虚virtual函数同样可以进行重载,但是重载只能是在当前自己名字空间作用域内有效
到底创建了几个String对象?
我们首先来看一段代码:
String str=new String("abc");&
紧接着这段代码之后的往往是这个问题,那就是这行代码究竟创建了几个String对象呢?相信大家对这道题并不陌生,答案也是众所周知的,2个。接下来我们就从这道题展开,一起回顾一下与创建String对象相关的一些JAVA知识。
我们可以把上面这行代码分成String str、=、"abc"和new String()四部分来看待。String str只是定义了一个名为str的String类型的变量,因此它并没有创建对象;=是对变量str进行初始化,将某个对象的引用(或者叫句柄)赋值给它,显然也没有创建对象;现在只剩下new String("abc")了。那么,new String("abc")为什么又能被看成"abc"和new String()呢?我们来看一下被我们调用了的String的构造器:
public String(String original) {&
&&& //other code ...&
大家都知道,我们常用的创建一个类的实例(对象)的方法有以下两种:
使用new创建对象。
调用Class类的newInstance方法,利用反射机制创建对象。
我们正是使用new调用了String类的上面那个构造器方法创建了一个对象,并将它的引用赋值给了str变量。同时我们注意到,被调用的构造器方法接受的参数也是一个String对象,这个对象正是"abc"。由此我们又要引入另外一种创建String对象的方式的讨论――引号内包含文本。
这种方式是String特有的,并且它与new的方式存在很大区别。
String str="abc";&
毫无疑问,这行代码创建了一个String对象。
String a="abc";&
String b="abc";&
那这里呢?答案还是一个。
String a="ab"+"cd";&
再看看这里呢?答案仍是一个。有点奇怪吗?说到这里,我们就需要引入对字符串池相关知识的回顾了。
在JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象,并且可以被共享使用,因此它提高了效率。由于String类是final的,它的值一经创建就不可改变,因此我们不用担心String对象共享而带来程序的混乱。字符串池由String类维护,我们可以调用intern()方法来访问字符串池。
我们再回头看看String a="abc";,这行代码被执行的时候,JAVA虚拟机首先在字符串池中查找是否已经存在了值为"abc"的这么一个对象,它的判断依据是String类equals(Object obj)方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用;如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。因此,我们不难理解前面三个例子中头两个例子为什么是这个答案了。
对于第三个例子:
String a="ab"+"cd";&
由于常量的值在编译的时候就被确定了。在这里,"ab"和"cd"都是常量,因此变量a的值在编译时就可以确定。这行代码编译后的效果等同于:
String a="abcd";&
因此这里只创建了一个对象"abcd",并且它被保存在字符串池里了。
现在问题又来了,是不是所有经过“+”连接后得到的字符串都会被添加到字符串池中呢?我们都知道“==”可以用来比较两个变量,它有以下两种情况:
如果比较的是两个基本类型(char,byte,short,int,long,float,double,boolean),则是判断它们的值是否相等。
如果表较的是两个对象变量,则是判断它们的引用是否指向同一个对象。
下面我们就用“==”来做几个测试。为了便于说明,我们把指向字符串池中已经存在的对象也视为该对象被加入了字符串池:
public class StringTest {
public static void main(String[] args) {
String a = "ab";// 创建了一个对象,并加入字符串池中
System.out.println("String a = \"ab\";");
String b = "cd";// 创建了一个对象,并加入字符串池中
System.out.println("String b = \"cd\";");
String c = "abcd";// 创建了一个对象,并加入字符串池中
String d = "ab" + "cd";
// 如果d和c指向了同一个对象,则说明d也被加入了字符串池
if (d == c) {
System.out.println("\"ab\"+\"cd\" 创建的对象 \"加入了\" 字符串池中");
// 如果d和c没有指向了同一个对象,则说明d没有被加入字符串池
System.out.println("\"ab\"+\"cd\" 创建的对象 \"没加入\" 字符串池中");
String e = a + "cd";
// 如果e和c指向了同一个对象,则说明e也被加入了字符串池
if (e == c) {
System.out.println(" a +\"cd\" 创建的对象 \"加入了\" 字符串池中");
// 如果e和c没有指向了同一个对象,则说明e没有被加入字符串池
System.out.println(" a +\"cd\" 创建的对象 \"没加入\" 字符串池中");
String f = "ab" +
// 如果f和c指向了同一个对象,则说明f也被加入了字符串池
if (f == c) {
System.out.println("\"ab\"+ b
创建的对象 \"加入了\" 字符串池中");
// 如果f和c没有指向了同一个对象,则说明f没有被加入字符串池
System.out.println("\"ab\"+ b
创建的对象 \"没加入\" 字符串池中");
String g = a +
// 如果g和c指向了同一个对象,则说明g也被加入了字符串池
if (g == c) {
System.out.println(" a + b
创建的对象 \"加入了\" 字符串池中");
// 如果g和c没有指向了同一个对象,则说明g没有被加入字符串池
System.out.println(" a + b
创建的对象 \"没加入\" 字符串池中");
运行结果如下:
String a = "ab";
String b = "cd";
"ab"+"cd" 创建的对象 "加入了" 字符串池中
a& +"cd" 创建的对象 "没加入" 字符串池中
"ab"+ b&& 创建的对象 "没加入" 字符串池中
a& + b&& 创建的对象 "没加入" 字符串池中
从上面的结果中我们不难看出,只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中。对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中,对此我们不再赘述。
但是有一种情况需要引起我们的注意。请看下面的代码:
public class StringStaticTest {
public static final String A = "ab";
public static final String B = "cd";
public static void main(String[] args) {
// 将两个常量用+连接对s进行初始化
String s = A + B;
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
System.out.println("s不等于t,它们不是同一个对象");
这段代码的运行结果如下:
s等于t,它们是同一个对象
这又是为什么呢?原因是这样的,对于常量来讲,它的值是固定的,因此在编译期就能被确定了,而变量的值只有到运行时才能被确定,因为这个变量可以被不同的方法调用,从而可能引起值的改变。在上面的例子中,A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说:
String s=A+B;&
String s="ab"+"cd";&
我对上面的例子稍加改变看看会出现什么情况:
public class StringStaticTest {
public static final String A;
public static final String B;
public static void main(String[] args) {
// 将两个常量用+连接对s进行初始化
String s = A + B;
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
System.out.println("s不等于t,它们不是同一个对象");
它的运行结果是这样:
s不等于t,它们不是同一个对象
只是做了一点改动,结果就和刚刚的例子恰好相反。我们再来分析一下。A和B虽然被定义为常量(只能被赋值一次),但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了。
由于字符串池中对象的共享能够带来效率的提高,因此我们提倡大家用引号包含文本的方式来创建String对象,实际上这也是我们在编程中常采用的。
接下来我们再来看看intern()方法,它的定义如下:
public native String intern();&
这是一个本地方法。在调用这个方法时,JAVA虚拟机首先检查字符串池中是否已经存在与该对象值相等对象存在,如果有则返回字符串池中对象的引用;如果没有,则先在字符串池中创建一个相同值的String对象,然后再将它的引用返回。
我们来看这段代码:
public class StringInternTest {
public static void main(String[] args) {
// 使用char数组来初始化a,避免在a被创建之前字符串池中已经存在了值为"abcd"的对象
String a = new String(new char[] { 'a', 'b', 'c', 'd' });
String b = a.intern();
if (b == a) {
System.out.println("b被加入了字符串池中,没有新建对象");
System.out.println("b没被加入字符串池中,新建了对象");
运行结果:
b没被加入字符串池中,新建了对象
如果String类的intern()方法在没有找到相同值的对象时,是把当前对象加入字符串池中,然后返回它的引用的话,那么b和a指向的就是同一个对象;否则b指向的对象就是JAVA虚拟机在字符串池中新建的,只是它的值与a相同罢了。上面这段代码的运行结果恰恰印证了这一点。
最后我们再来说说String对象在JAVA虚拟机(JVM)中的存储,以及字符串池与堆(heap)和栈(stack)的关系。我们首先回顾一下堆和栈的区别:
栈(stack):主要保存基本类型(或者叫内置类型)(char、byte、short、int、long、float、double、boolean)和对象的引用,数据可以共享,速度仅次于寄存器(register),快于堆。
堆(heap):用于存储对象。
我们查看String类的源码就会发现,它有一个value属性,保存着String对象的值,类型是char[],这也正说明了字符串就是字符的序列。
当执行String a="abc";时,JAVA虚拟机会在栈中创建三个char型的值'a'、'b'和'c',然后在堆中创建一个String对象,它的值(value)是刚才在栈中创建的三个char型值组成的数组{'a','b','c'},最后这个新创建的String对象会被添加到字符串池中。如果我们接着执行String b=new String("abc");代码,由于"abc"已经被创建并保存于字符串池中,因此JAVA虚拟机只会在堆中新创建一个String对象,但是它的值(value)是共享前一行代码执行时在栈中创建的三个char型值值'a'、'b'和'c'。
说到这里,我们对于篇首提出的String str=new String("abc")为什么是创建了两个对象这个问题就已经相当明了了。
您可能感兴趣的文章:
上一篇:下一篇:
最 近 更 新
热 点 排 行
12345678910

我要回帖

更多关于 vb定义变量 的文章

 

随机推荐