
大四在校生
参考书籍:《第一行代码》 第二版 郭霖 开发工具:AndroidStudio 3.2 Stable Channel 如有错漏,请批评指出! Activity 定义:Activity是Android的四大组件之一,它是一种可以包含用户界面的组件,主要用于和用户进行交互。 手动创建一个Activity的三个步骤 step 1:创建Activity 在项目目录的 app\src\main\java\包名 右键->New->Activity->Empty Activity 创建Activity 先取消Generate Layout File选项的勾选,输入Activity Name ,然后点击 Finish 按钮。这样就完成了第一步。 补充说明: 勾选Generate Layout File表示自动为这个Activity创建一个对应的布局文件(默认勾选)。 勾选Launcher Activity表示自动将这个Activity设置为当前项目的主Activity(每个项目只能有一个主Activity,主Activity即启动app时显示的第一个Activity)。 勾选Backwards Compatibility(AppCompat)表示会为项目启用向下兼容的模式(默认勾选)。 Step 2:创建和加载布局文件 在项目目录的 app\src\main\res\layout 右键->New->XML->Layout XML File 创建布局文件 接下来输入Layout File Name,Root Tags 表示布局文件的根布局,这里默认LinearLayout(线性布局),后面会介绍关于布局的内容。 然后在NewActivity.java文件中为这个活动加载我们创建的布局(添加圈出的代码): 加载布局 其实就是将我们创建的Activity和布局文件关联起来,这样我们打开这个Activity 时显示的就是activity_new这个布局文件所编写的内容。 Step3:注册Activity 打开项目目录中 app\src\main\AndroidManifest.xml 文件,在这里为我们创建的Activity进行注册(实际上,在第一步我们创建这个Activity时就已经自动注册了)。 注册Activity 看起来这一步似乎是多余的,这是因为Android Studio自动帮我们注册了Activity。但是,如果我们的Activity不是通过上面的步骤创建的,而是从别的地方复制到项目中来的,那么我们就需要谨记要自己在AndroidManifest文件中为这个活动进行注册,否则就会出错。 这样,我们就完成了手动创建一个Activity的流程。实际上,在第一步我们只要勾选上Generate Layout File 选项,后面两步AndroidStudio都会自动为我们完成。 补充说明: 关于主Activity,在AndroidManifest.xml文件(上图)中我们可以看到,在MainActivity的标签下,有这样几行代码: <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> 实际上,在<activity>标签内部加入<intent-filter>标签,并添加上面的两句声明,就是将这个Activity指定为主Activity。我们可以在创建Activity时勾选上Launcher Activity,Android Studio就会自动为我们配置这个Activity为主Activity,当然,也可以自己在需要指定为主Activity对应的注册标签<activity>中添加上面的代码。 Toast的使用 Toast 是Android系统提供的一种非常好的提醒方式,在程序中可以使用它将一些短小的信息通知给用户,这些信息会在一段时间后自动消失,并且不会占用任何屏幕空间。 Toast的用法非常简单,通过静态方法makeText()创建出一个Toast对象,然后调用show()方法将Toast显示出来就可以了。 @Override public void onClick(View v) { switch (v.getId()){ case R.id.show_toast_button: Toast.makeText(SecondActivity.this, "You clicked button", Toast.LENGTH_SHORT).show(); break; default: break; } } 上面的代码表示通过点击我们指定的Button触发Toast弹出,我们可以看到makeText()方法有三个参数。 第一个参数是Context,即Toast要求的上下文,我们暂且只需要知道Context是一个抽象类,而Activity是它的子类,因此我们指定的SecondActivity.this就是一个Context对象。 第二个参数是我们要显示的提示信息。 第三个参数是Toast显示的时长,有两个内置常量Toast.LENGTH_SHORT和Toast.LENGTH_LONG可以选择。 效果图如下: 效果图.gif Menu的使用 由于手机屏幕空间十分有限,因此Android提供了Menu,用于展示菜单的同时,也不占用过多屏幕空间。 使用方法: step1:创建菜单布局 首先在res目录下新建一个menu文件夹,用于存放Menu布局文件,右键res->New->Directory,输入文件夹名menu;然后右键menu->New-Menu resource file,输入文件名,点击OK完成。 创建Menu布局文件 接下来根据需要创建菜单项,这里我们创建三个菜单项,代码如下: <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/add_item" android:title="@string/add"/> <item android:id="@+id/del_item" android:title="@string/del"/> <item android:id="@+id/modify_item" android:title="@string/modify"/> </menu> <item>标签就是用来创建具体的某个菜单项; android:id标签是用来给这个菜单项指定一个唯一的标识符; android:title标签是给这个菜单项指定一个名称。 step2:为当前活动创建菜单——重写onCreateOptionsMenu()方法 由于我们要给SecondActivity添加Menu,因此我们在SecondActivity.java文件中重写onCreateOptionsMenu()方法。 @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.second, menu); return true; } 通过getMenuInflater()方法获取到MenuInflater对象,再调用它的inflate()方法,给当前Activity创建菜单。inflate()方法接收两个参数: 第一个参数 用来指定第一步创建的布局文件; 第二个参数用于指定我们的菜单项添加到那个Menu对象中,这里自然是onCreateOptionsMenu()方法传入的menu对象,表示当前Activity的Menu。 返回值为true表示允许菜单显示;否则菜单不显示。 step3:为菜单定义响应事件——重写onOptionsItemSelected()方法 在SecondActivity中重写onOptionsItemSelected()方法,通过item.getItenId()方法来判断我们点击的是哪个菜单项,并为其添加逻辑处理,这里我们弹出一个Toast。 @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()){ case R.id.add_item: Toast.makeText(SecondActivity.this, "添加", Toast.LENGTH_SHORT).show(); break; case R.id.del_item: Toast.makeText(SecondActivity.this, "删除", Toast.LENGTH_SHORT).show(); break; case R.id.modify_item: Toast.makeText(SecondActivity.this, "修改", Toast.LENGTH_SHORT).show(); break; default: break; } return true; } 效果图如下: 效果图 使用Intent实现活动间跳转 显式Intent 显式Intent的使用十分简单,直接上代码: @Override public void onClick(View v) { Intent intent = null; switch (v.getId()){ default: break; case R.id.ll_button: intent = new Intent(MainActivity.this,FirstActivity.class); startActivity(intent); break; case R.id.l2_button: intent = new Intent(MainActivity.this,SecondActivity.class); startActivity(intent); break; } } 首先,我们直接new一个Intent对象,构造方法接收两个参数,第一个参数是上下文,因为当前Activity是MainActivity,所以我们传入MainActivity.this,第二个参数指定想要启动的目标活动,这里实际上有两个button,我们分别传入FirstActivity.class和SecondActivity.class,所以点击两个button时会分别跳转到FirstActivity和SecondActivity。下面看效果: 效果图.gif 隐式Intent 相比于显式Intent,隐式Intent则含蓄了很多,它并明确指出我们要启动那个Activity,而是通过指定一系列action和category等信息,然后由系统进行分析,继而启动合适的活动。 在这里我用两个Activity进行对比,通过下面的步骤对要启动的Activity进行配置,并实现隐式启动: step1:为Activity配置action和category 在AndroidManifest文件中,我们给ThirdActivity和NewActivity分别配置<action>标签和<category>标签: <activity android:name=".ThirdActivity" android:label="@string/thirdactivity"> <intent-filter> <action android:name="com.example.aboutactivity.MY_ACTION"/> <category android:name="android.intent.category.DEFAULT"/> </intent-filter> </activity> <activity android:name=".NewActivity" android:label="@string/new_activity"> <intent-filter> <action android:name="com.example.aboutactivity.MY_ACTION"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.MY_CATEGORY"/> </intent-filter> </activity> 我们给ThirdActivity指定了一个<action>标签和一个<category>标签,给NewActivity指定了一个<action>标签和两个<category>标签,下面我们通过Intent其它的构造方法来隐式指定对应的标签。 step2:给Intent添加<action>和<category>标签 @Override public void onClick(View v) { Intent intent = null; switch (v.getId()){ case R.id.l3_button: intent = new Intent("com.example.aboutactivity.MY_ACTION"); startActivity(intent); break; case R.id.l4_button: intent = new Intent("com.example.aboutactivity.MY_ACTION"); intent.addCategory("android.intent.category.MY_CATEGORY"); startActivity(intent); break; default: break; } } 从上面的代码可以看出来,我们给第一个intent添加了“com.example.aboutactivity.MY_ACTION”这个action,给第二个intent添加了“com.example.aboutactivity.MY_ACTION”这个action和“android.intent.category.MY_CATEGORY”这个category,细心的话可能会注意到,两个intent添加的action是相同的,并且第一个intent没有添加category,这是因为“android.intent.category.DEFAULT”这个category是默认category,会自动添加。而且因为我们给第二个intent添加了额外的category标签,因此两个Activity的action和category可以区分开来,这一点是为了说明,我们需要给不同的Activity指定可以区分的<action>标签和<category>标签,否则系统识别不出我们要启动的Activity。下面看效果吧! 隐式Intent.gif 从上面的效果图我们可以看出,由于两个Activity都具有相同的<action>和默认的<category>所以当我们点击ThirdActivity这个button时,系统询问我们要启动哪个Activity,我想这个现象在我们使用手机的时候也遇到过,而由于NewActivity具有自己独有的<category>标签“android.intent.category.MY_CATEGORY”,因此我们点击NewActivity这个bitton时,系统准确的启动了NewActivity,这就是隐式Intent的奇妙之处了! Activity之间的数据传递 向下一个Activity传递数据 前面我们知道Intent可以用来启动Activity,其实通过Intent我们还可以向下一个Activity传递数据,Intent提供了一系列putExtra()方法的重载,我们可以把想要传递的数据暂存在Intent中,启动另一个Activity后,我们只需要再将数据取出来,就可以了。接下来我们结合Toast以及前面创建好的MainActivity和SecondActivity来做一个演示。 step1:在第一个Activity中将数据暂存到Intent中 首先我们就在MainActivity中将数据暂存到Intent中,代码如下: @Override public void onClick(View v) { Intent intent = null; switch (v.getId()){ case R.id.l2_button: intent = new Intent(MainActivity.this,SecondActivity.class); intent.putExtra("Integer",1024); intent.putExtra("String","Hello World!"); startActivity(intent); break; default: break; } } 数据以键值对的形式暂存在Intent中,除了基本数据类型,还可以传递数组、字符串以及实现了serializable和Parcelable接口的对象。 step2:在启动的Activity中将数据取出 要取出数据,我们首先要通过getIntent()方法获取到用于启动SecondActivity的Intent对象,然后调用对应数据类型的get方法将数据取出。代码如下: @Override public void onClick(View v) { switch (v.getId()){ case R.id.show_toast_button: Intent intent = getIntent(); int num = 0; String str = null; if(intent != null){ num = intent.getIntExtra("Integer", 0); str = intent.getStringExtra("String"); } Toast.makeText(SecondActivity.this, str+"\t"+num , Toast.LENGTH_SHORT).show(); break; default: break; } } 我们在SecondActivity里面通过一个button触发Toast将传递过来的数据展示出来,下面看效果: 向下一个Activity传递数据.gif 返回数据给上一个活动 向下一个Activity传递数据十分简单,接下来说一说如何返回数据给上一个Activity。我们可以模拟这样一个场景:在前面我们通过MainActivity启动了SecondActivity,并同时向其传递了一些数据,那么假设我们在SecondActivity中收到数据时,要返回一个接收到数据的确认消息给MainActivity该怎么实现呢?Activity中有一个startActivityForResult()方法,显然这就是我们所需要的。 step1:通过startActivityFprResult()方法启动Activity 我们来修改MainActivity中的代码: @Override public void onClick(View v) { Intent intent = null; switch (v.getId()){ case R.id.l2_button: intent = new Intent(MainActivity.this,SecondActivity.class); intent.putExtra("Integer",1024); intent.putExtra("String","Hello World!"); startActivityForResult(intent,1024); break; default: break; } } startActivityForResult()方法接收两个参数,第一个是intent,第二个是请求码,用于在后面判断数据的来源。我们先往下看。 step2:在启动的Activity中添加返回数据的逻辑 我们首先在SecondActivity中添加一个button,然后为其添加点击事件。在这里我们要构建一个Intent用于传递数据,将要传递的数据存放到Intent中,然后调用setResult()方法。代码如下: @Override public void onClick(View v) { switch (v.getId()){ case R.id.data_back_button: Intent intent = new Intent(); intent.putExtra("Confirm","收到数据"); setResult(RESULT_OK, intent); finish(); break; default: break; } } 上面的逻辑就是将要返回的数据存放在Intent中,然后调用setResult()方法将数据返回给上一个Activity,setResult()方法接收两个参数,第一个参数用于向上一个Activity返回处理结果,一般只用RESULT_OK和RESULT_CANCELED这两个值。然后调用finish()方法将当前Activity结束,返回上一个Activity。当然,触发条件是点击刚才定义的Button。 step3:获取返回的数据 既然数据已经返回到MainActivity了,并且我们现在也回到MainActivity里面了,那么我们怎么知道返回的数据呢?因为我们是通过startActivityForResult()方法启动的SecondActivity,所以当我们再次回到MainActivity时,会回调onActivityResult()方法。也就是说,通过重写onActivityResult()方法,我们可以对返回的数据进行处理。下面看代码: @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { switch (requestCode){ case 1024: if (resultCode == RESULT_OK && data != null ){ String str = data.getStringExtra("Confirm"); Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show(); } break; default: break; } } onActivityResult()方法的三个参数分别是我们前面传入的请求码、返回数据时传入的处理结果和携带返回数据的Intent对象。可以看到,前面我们提到的请求码和结果码都派了用场,下面看效果: 向上一个Activity返回数据.gif Activity实际场景应用 判断当前是在哪个Activity 当我们在看别人的项目时,经常会遇上这么一个问题:这个app这么多页面,究竟每一个对应哪个Activity呢?其实想要解决这个问题并不难,下面我们来学习一个小技巧。 step1:创建一个BaseActivity类 为什么是BaseActivity类呢?因为这个BaseActivity并不是一个要显示出来的Activity,它只是作为我们Activity的基类,提供一些方法,从而实现我们的目的,并且让它继承自AppCompatActivity。右键包名->New->JavaClass 创建BaseActivity 然后重写onCreate()方法,这里为了演示效果,我们用Toast弹出当前Activity实例的类名。(在实际项目中我们用Log.d()打印出来就行了)代码如下: @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Toast.makeText(BaseActivity.this, getClass().getSimpleName(), Toast.LENGTH_SHORT).show(); } step2:让项目中所有的Activity都继承自BaseActivity 由于我们的BaseActivity是继承自AppCompatActivity的,因此这个修改不会带来什么影响。我们直接看运行效果吧! 知晓当前Activity.gif 随时退出应用 当我们在写一个由很多Activity组成的项目时,很可能要面临一个问题,从ActivityA打开ActivityB,然后从ActivityB打开ActivityC,这时如果我们按Back键,会依次回退到ActivityB->ActivityA->主界面,按Home键也只是返回桌面,并没有关闭应用。但是如果我们在ActivityC需要直接关闭掉这个应用呢?其实也不难,我们只需要创建一个ActivityCollector类作为Activity管理器,这个问题就迎刃而解啦! step1:创建ActivityCollector类,并添加相关方法实现 public class ActivityCollector { private static List<Activity> activities = new ArrayList<>(); public static void addActivity(Activity activity){ activities.add(activity); } public static void removeActivity(Activity activity){ activities.remove(activity); } public static void finishAllActivity(){ for (Activity activity : activities){ if (!activity.isFinishing()){ activity.finish(); } } activities.clear(); } } step2:重写BaseActivity中的相关方法 既然创建了ActivityCollector类,那么我们每创建一个Activity,就要添加到我们的Activity管理器中,每销毁一个Activity,也要从Activity管理器中将之移除。下面是代码: public class BaseActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Toast.makeText(BaseActivity.this, getClass().getSimpleName(), Toast.LENGTH_SHORT).show(); ActivityCollector.addActivity(this); } @Override protected void onDestroy() { super.onDestroy(); ActivityCollector.removeActivity(this); } } step3:在需要直接退出程序的Activity中添加相关逻辑 例如,我们要在某个Activity中通过点击某个button来关闭应用,那么我们只需要在这个Button的点击事件中调用ActivityCollector.finishAllActivity()就可以啦!关于这个就不写相关示例和演示了,自己动手,印象更深刻! 关于Activity的生命周期和启动模式我单独写了两篇博客:Android笔记(一) | Activity的生命周期Android笔记(二) | Activity的启动模式
最初学习Android的时候,是边学习边做着一个小项目的,因为项目需求,需要实现一个底部导航栏的功能,由于基础知识受限,百度了很多博客,大致就找到两种实现方案:第一种就是直接用Fragment实现(点击切换),第二种是ViewPager+Fragment实现(除了点击切换,还支持左右滑动切换)。根据需求使用了第一种方法,后期产生了Fragment重叠的问题,由于这个bug时而出现,也不知道如何定位(学生时期),就暂且放下了。现在因为学习进度(系统学习Fragment),重新捡起这个问题,就想写一篇实现功能+解决bug的博客,如有不足之处,请留言指教。 实现思路 当我们进入Activity时,首先展示第一个页面,即创建对应Fragment实例,使用add+show方法显示出来,当我们点击进入别的页面时,调用hide方法将已展示的Fragment页面隐藏(实际是设置Visiable属性为不可见),然后显示对应Fragment页面(已创建则直接调用show方法,未创建则创建,然后调用add+show方法显示)。 这里补充一点:切换页面也可以用replace方法,它和hide+show方法的直观区别就是:使用replace方法会先将fragment实例remove掉,然后重新add,这就导致Fragment每次切换都会重新走一遍生命周期,创建一个新的实例,不会保存每个Fragment的状态;而使用hide+show方法则仅仅是将不显示的Fragment设置为不可见,再次显示出来时会保存状态。 先上效果图 接下来就是代码咯 首先创建一个Activity,写好页面布局。 首先新建一个Layout Xml File bottombar.xml <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/colorBackGround" android:paddingTop="5dp" android:paddingBottom="5dp"> <TextView android:id="@+id/text_clothes" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:drawableTop="@drawable/ic_clothes" android:text="@string/clothes" android:textSize="13sp" android:gravity="center_horizontal"/> <TextView android:id="@+id/text_food" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:drawableTop="@drawable/ic_food" android:text="@string/food" android:textSize="13sp" android:gravity="center_horizontal"/> <TextView android:id="@+id/text_hotel" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:drawableTop="@drawable/ic_hotel" android:text="@string/hotel" android:textSize="13sp" android:gravity="center_horizontal"/> </LinearLayout> 在activity_main.xml中使用include标签引用 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <FrameLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"/> <View android:background="@color/colorGray" android:layout_width="match_parent" android:layout_height="0.1dp"/> <include layout="@layout/bottombar"/> </LinearLayout> 布局写好了,接下来新建三个空白Fragment(这里只列出一个) ClothesFragment.java import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.example.laughter.testdemo.R; /** * A simple {@link Fragment} subclass. */ public class ClothesFragment extends Fragment { public ClothesFragment() { // Required empty public constructor } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_clothes, container, false); } } fragment_clothes.xml <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".fragment.ClothesFragment"> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:text="@string/clothes" android:textSize="72sp" android:gravity="center"/> </FrameLayout> 我这里为了区分Fragment页面给每个页面添加了一个TextView,具体就根据自己的需求在Fragment中写代码就行了。 接下来就是在MainActivity中写逻辑控制Fragment的切换了。 import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.TextView; import com.example.laughter.testdemo.fragment.ClothesFragment; import com.example.laughter.testdemo.fragment.FoodFragment; import com.example.laughter.testdemo.fragment.HotelFragment; public class MainActivity extends AppCompatActivity implements View.OnClickListener{ //底部菜单栏3个TextView private TextView mTextClothes; private TextView mTextFood; private TextView mTextHotel; //3个Fragment private Fragment mClothesFragment; private Fragment mFoodFragment; private Fragment mHotelFragment; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //初始化 init(); //设置第一个Fragment默认显示 setFragment(0); } @Override public void onClick(View v) { switch (v.getId()){ default: break; case R.id.text_clothes: setFragment(0); break; case R.id.text_food: setFragment(1); break; case R.id.text_hotel: setFragment(2); break; } } private void init(){ //初始化控件 mTextClothes = (TextView)findViewById(R.id.text_clothes); mTextFood = (TextView)findViewById(R.id.text_food); mTextHotel = (TextView)findViewById(R.id.text_hotel); //设置监听 mTextClothes.setOnClickListener(this); mTextFood.setOnClickListener(this); mTextHotel.setOnClickListener(this); } private void setFragment(int index){ //获取Fragment管理器 FragmentManager mFragmentManager = getSupportFragmentManager(); //开启事务 FragmentTransaction mTransaction = mFragmentManager.beginTransaction(); //隐藏所有Fragment hideFragments(mTransaction); switch (index){ default: break; case 0: //设置菜单栏为选中状态(修改文字和图片颜色) mTextClothes.setTextColor(getResources() .getColor(R.color.colorTextPressed)); mTextClothes.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.ic_clothes_pressed,0,0); //显示对应Fragment if(mClothesFragment == null){ mClothesFragment = new ClothesFragment(); mTransaction.add(R.id.container, mClothesFragment, "clothes_fragment"); }else { mTransaction.show(mClothesFragment); } break; case 1: mTextFood.setTextColor(getResources() .getColor(R.color.colorTextPressed)); mTextFood.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.ic_food_pressed,0,0); if(mFoodFragment == null){ mFoodFragment = new FoodFragment(); mTransaction.add(R.id.container, mFoodFragment, "food_fragment"); }else { mTransaction.show(mFoodFragment); } break; case 2: mTextHotel.setTextColor(getResources() .getColor(R.color.colorTextPressed)); mTextHotel.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.ic_hotel_pressed,0,0); if(mHotelFragment == null){ mHotelFragment = new HotelFragment(); mTransaction.add(R.id.container, mHotelFragment, "hotel_fragment"); }else { mTransaction.show(mHotelFragment); } break; } //提交事务 mTransaction.commit(); } private void hideFragments(FragmentTransaction transaction){ if(mClothesFragment != null){ //隐藏Fragment transaction.hide(mClothesFragment); //将对应菜单栏设置为默认状态 mTextClothes.setTextColor(getResources() .getColor(R.color.colorText)); mTextClothes.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.ic_clothes,0,0); } if(mFoodFragment != null){ transaction.hide(mFoodFragment); mTextFood.setTextColor(getResources() .getColor(R.color.colorText)); mTextFood.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.ic_food,0,0); } if(mHotelFragment != null){ transaction.hide(mHotelFragment); mTextHotel.setTextColor(getResources() .getColor(R.color.colorText)); mTextHotel.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.ic_hotel,0,0); } } } 上面代码中逻辑很清晰,根据注释基本可以看明白,具体有些控件的用法自行百度。到这里功能就已经实现了,但是会出现Fragment重叠的bug。具体情况如下图: Fragment重叠异常 出现这种情况是什么原因呢?了解Activity的生命周期的话,你就知道默认情况下,当我们旋转屏幕时,Activity会销毁重建,这个过程属于异常情况下的生命周期,系统会调用onSaveInstanceState和onRestoreInstanceState方法保存并恢复Activity的状态,而Fragment也在恢复的内容之中。但是在之前的Activity中我们创建了Fragment的实例,并且add到FragmentTransaction中了,这些实例在Activity重建时并没有remove,只不过Activity重建之后,没有对象指向它们,也就是说,在重建后的Activity中,我们创建的3个fragmeng对象是指向null的。 所以,在重建后的的Activity中,又会重新创建Fragment的实例,并且显示出来,而之前被系统恢复的Fragment也会恢复之前的显示状态,这就导致了多个Fragment重叠。当然,任何能导致Activity销毁重建的情况都会产生这个bug,比如说应用在后台时,因为内存资源不足导致Activity被kill。既然知道原因了,那么解决起来就不难了。 这里我想到的解决办法是从重新创建Fragment这里着手,既然保存的状态会恢复,那么Activity重建的时候我们不让Fragment重新创建不就行了。具体怎么做呢?这里还是需要熟悉Activity的生命周期。 首先,我们要在onCreate方法中添加一个判断,当Activity不是异常终止并恢复(即savedInstanceState == null)时,才显示默认Fragment。 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //初始化 init(); //根据传入的Bundle对象判断Activity是正常启动还是销毁重建 if(savedInstanceState == null){ //设置第一个Fragment默认选中 setFragment(0); } } 然后,我们要获取到原本已经创建的Fragmen实例,并让重建后的Activity中的Fragment对象指向它们。并且设置一个变量,用于保存在销毁重建前显示的是哪个Fragment。这两个操作需要重写onSaveInstanceState方法和onRestoreInstanceState方法。(这里只贴出修改过的代码) public class MainActivity extends AppCompatActivity implements View.OnClickListener{ ... //标记当前显示的Fragment private int fragmentId = 0; ... @Override protected void onSaveInstanceState(Bundle outState) { //通过onSaveInstanceState方法保存当前显示的fragment outState.putInt("fragment_id",fragmentId); super.onSaveInstanceState(outState); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); FragmentManager mFragmentManager = getSupportFragmentManager(); //通过FragmentManager获取保存在FragmentTransaction中的Fragment实例 mClothesFragment = (ClothesFragment)mFragmentManager .findFragmentByTag("clothes_fragment"); mFoodFragment = (FoodFragment)mFragmentManager .findFragmentByTag("food_fragment"); mHotelFragment = (HotelFragment)mFragmentManager .findFragmentByTag("hotel_fragment"); //恢复销毁前显示的Fragment setFragment(savedInstanceState.getInt("fragment_id")); } ... 修改之后,我们可以打印出Fragment对象,看看销毁重建后与重建前它们是否指向同一个实例。 显然,销毁重建后Fragment对象所指向的实例与重建前相同。这样我们的BottomBar就完成了! 想看源码点击这里 如果对Activity生命周期不太了解,可以看一看我的另一篇博客:Android笔记(一) | Activity的生命周期
最近想总结一下常用的几种排序算法,恰好看到一系列总结的很好的博客,感觉博主做的很用心,分享一下。 图解排序算法(一)之3种简单排序(选择,冒泡,直接插入)图解排序算法(二)之希尔排序图解排序算法(三)之堆排序图解排序算法(四)之归并排序图解排序算法(五)之快速排序 附上我自己的代码实现(C++)选择排序、冒泡排序、直接插入排序、希尔排序堆排序归并排序快速排序 补充: 稳定性的定义:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。 上一篇:数据结构与算法(三) 线性表之双向链表
概述 在JVM的运行时数据区中,程序计数器、JVM栈和本地方法栈随线程而生,随线程而灭,内存分配和回收具备确定性,因此这几个区域不需要过多考虑内存回收问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序处于运行期才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关心的是这部分内存。 对象存活判定算法 在堆中存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象中哪些还“存活”着,那些已经“死去”(即不可能再被任何途径使用的对象) 1. 引用的概念 在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用和虚引用四种,这四种引用强度依次减弱。 强引用(Strong Reference) 指创建一个对象并把这个对象赋值给一个引用变量Object obj = new Object(); 只要强引用存在,即使抛出OutOfMemoryError异常,垃圾收集器也不会回收被引用的对象。 软引用(Soft Referrence) 用来描述一些还有用但非必须的对象。 对于软引用关联的对象,在内存不足时会被回收。 弱引用(Weak Reference) 用来描述非必须对象,但强度比软引用更弱。 无论当前内存是否足够,都会被回收。 虚引用(Phantom Reference) 不影响对象生存时间,也无法通过虚引用取得对象实例。 存在意义在于与引用队列关联使用,判断被虚引用关联的对象是否即将被回收。 推荐阅读:Java的四种引用方式 2. 引用计数算法 实现方式:给对象中添加一个引用计数器,每当有一个地方去引用它,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器值为0的对象就是不可能再被使用的。 这种算法实现简单、判定效率高,但是很难解决对象之间相互循环引用的问题。 虚拟机不是通过引用计数算法来判断对象是否存活。 3. 可达性分析算法 实现方式:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,证明此对象是不可引用的。 在Java中,可作为GC Roots的对象包括以下几种: JVM栈(栈帧中的本地变量表)中引用的对象; 方法区中类的静态属性引用的对象; 方法区中常量引用的对象; 本地方法栈中JNI(即常说的Native方法)引用的对象。 当然,可达性分析算法中不可达的对象并不是一定会被回收,如果这个对象在执行finalize()方法时,重新与引用链上的对象关联起来,就会被移除出“即将回收”集合。 4. 方法区回收 JVM规范中说过可以不要求JVM在方法区实现垃圾回收,方法区的垃圾回收效率十分低。 方法区的垃圾回收主要回收两部分内容: 废弃常量的回收与Java堆中对象的回收十分类似,即没有被任何其他地方引用就会被回收。 满足以下条件会被判定为无用的类 该类所有的实例都已经被回收,即Java堆中不存在该类的任何实例; 加载该类的 ClassLoader 已经被回收; 该类对应的 java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 垃圾收集算法 首先可以先看看下面这篇博客,了解一下新生代和老年代的概念:新生代和老年代 接下来介绍几种垃圾收集算法。 1. 复制算法 算法思想:将可用内存按容量划分为大小相等的两块,每次只使用其中给一块,当这一块的内存用完了,就将还存活的对象复制到另一块,再将已使用的一块内存全部清理掉。 优点:每次都对整个半区进行回收,不会产生内存碎片,实现简单,运行高效。 缺点:相当于将可用内存缩小为一半,使用率太低。 2. 标记—清除算法 算法思想:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。 不足: 标记和清除两个过程的效率都不高; 标记清除后会产生大量不连续的内存碎片,造成后面无法为较大对象分配空间,频繁触发垃圾收集,影响系统性能。 3. 标记—整理算法 算法思想:首先标记出所有需要回收的对象,然后将所有存活的对象移动到一端,然后直接清理端边界以外的全部内存。 优点:可以应对大量对象存活,只有少量内存需要回收的情况,适合老年代使用。 4. 分代收集算法 这种算法被当代虚拟机广泛使用。 算法思想:根据对象存活周期将Java堆分为新生代和老年代,分别使用合适的算法进行垃圾收集。 新生代对象存活率低,使用复制算法,只需付出少量存活对象的复制成本就可以完成收集。 IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”,所以可以将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor空间。回收时,将这两块空间中存活的对象复制到另一块Survivor空间中,最后清理掉Eden和Survivor空间。这样空间使用率就达到了90%。当然我们不能保证每次都只有不多于10%的对象存活,这时就需要依赖老年代空间进行分配担保,即让survivor空间存放不下的对象通过分配担保机制进入老年代。 老年代对象存活率高,使用标记—清理算法或者标记—整理算法。 上一篇:JVM笔记 | Java内存管理
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。 《深入理解Java虚拟机》 概述 对于一个Java程序员而言,由于JVM的自动内存管理机制,不需要为每一个new操作写对应的delete/free操作,也不容易出现内存泄露和内存溢出的问题。然而一旦出现内存泄漏和溢出方面的问题,如果对JVM内存管理机制不了解,那么排查错误将十分艰难。 运行时数据区域 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,即运行时数据区。这些区域都有各自的用途、以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有的区域依赖用户线程的启动和结束而建立和销毁。 1. 程序计数器(Program Counter Register) 是一块较小的内存空间,可看作当前线程所执行的字节码的行号指示器。 如果正在执行一个Java方法,计数器记录正在执行的虚拟机字节码指令的地址。 如果正在执行一个Native方法,计数器值为空(Undefined)。 Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。 为了切换线程后恢复到正确的执行位置,每条线程都需要独立的程序计数器,各线程计数器独立存储,互不影响。因此程序计数器是线程私有的内存 是唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域。 2. Java虚拟机栈(Java Virtual Machine Stacks) 描述Java方法执行的内存模型。 每个方法在执行的同时会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。 每个方法从调用至执行完成的过程,对应一个栈帧在虚拟机栈中入栈和出栈的过程。 是线程私有的,生命周期与线程相同。 经常有人把Java内存区分为堆内存和栈内存,这种分法比较粗糙。这种划分方式的流行只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块。 其中堆指“Java堆”,栈指“JVM栈”,或者说JVM栈中的局部变量表部分。 局部变量表存放了编译期可知的基本数据类型、对象引用和returnAddress类型(指向一条字节码指令的地址)。 局部变量表所需内存在编译器分配,在进入方法时完全确定,并且方法运行期间不会改变。 JVM规范中,对这个区域规定了两种异常情况: 如果线程请求的栈深度大于JVM所允许的深度,将抛出StackOverFlowError异常; 如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。 3. 本地方法栈(Native Method Stack) 与JVM栈区别在于JVM栈为虚拟机执行Java方法(字节码)服务,本地方法栈为虚拟机使用到的Native方法服务。 在JVM规范中没有强制规定,由虚拟机自由实现(例如Sun HotSpot虚拟机 将本地方法栈和JVM栈合二为一)。 与JVM栈相同,会抛出StackOverFlowError和OutOfMemoryError异常。 4. Java堆(Java Heap) Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,是JVM管理的内存中最大的一块(对大多应用程序而言)。 几乎所有的对象实例和数组都在Java堆分配内存。 Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”(Garbage Collected Heap)。从内存回收角度看,由于现在收集器基本都采用分代收集算法,所以Java堆还可细分为新生代和老年代;从内存分配角度看,Java堆可能划分出多个线程私有的分配缓冲区(TLAB),无论如何划分,都与存放内容无关,进一步划分的目的是为了更好地回收内存或更快地分配内存。 JVM规范规定:Java堆在物理上可以处于不连续内存空间中,只要逻辑上连续即可。如果在堆中没有内存完成实例分配,并且堆也无法拓展时,会抛出OutOfMemoryError异常。 5. 方法区(Method Area) 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,是线程共享的内存区域。 对于习惯在HotSpot虚拟机上开发、部署程序的开发者来说,很多人更愿意把方法区称为“永久代”,本质上两者并不等价,仅仅是因为HotSpot虚拟机使用永久代来实现方法区,虽然省去了专门为方法区编写内存管理代码的工作,但现在看来并不是一个好主意,因为这样更容易遇到内存溢出问题。 JVM规范对方法区的限制非常宽松,物理内存不需要连续,可以选择固定大小也可扩展,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域出现较少,这个区域的内存回收目标主要针对常量池的回收和类型的卸载。 JVM规范规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。 6. 运行时常量池(Runtime Constant Pool) Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。 运行时常量池是方法区的一部分,受到方法区内存的限制。 JVM规范对运行时常量池没有做任何细节的要求,虚拟机提供商可以按照自己的需求来实现这个内存区域。 运行时常量池相对于Class文件常量池的一个重要特征是具备动态性,并非只有Class文件常量池的内容才能进入运行时常量池,运行期间也可能将新的常量放入池中。(常见的如String类的intern()方法) 当常量池无法申请到内存时,会抛出OutOfMemoryError异常。推荐阅读博客:JVM基本结构 下一篇:JVM笔记 | Java垃圾回收(GC)
本文主要总结有关View的常用基础知识,作为V学习View相关内容的根基。 主要内容:View的位置参数、MotionEvent、TouchSlop对象、VelocityTracker、GestureDetector和Scroller对象。 View类 View类是Android中所有控件的基类,包括ViewGroup(控件组);这也就意味着,View本身可以是单个控件,也可以是包含多个控件的一组控件。 View的位置参数 View的位置主要由四个顶点决定,分别对应于四个属性:top、left、right、bottom。从Android3.0开始,增加了几个额外参数:x、y、translationx、translationY。这些参数都是相对于父容器的。 x:View发生平移后的左上角横坐标 y:View发生平移后的左上角纵坐标 translationX:View的横向偏移量(初始为0) translationY:View的纵向偏移量(初始为0) MotionEvent 手指触摸屏幕后的典型事件类型有: ACTION_DOWN——手指接触屏幕瞬间; ACTION_MOVE——手指在屏幕上滑动; ACTION_UP——手指离开屏幕瞬间; 通过MotionEvent对象可以得到点击事件发生的x、y坐标: getX()——获取触摸点相对于当前View左上角的横坐标; getY()——获取触摸点相对于当前View左上角的纵坐标; getRawX()——获取触摸点相对于手机屏幕左上角的横坐标; getRawY()——获取触摸点相对于手机屏幕左上角的纵坐标; TouchSlop TouchSlop是系统所能识别出的被认为是滑动的最小距离。 这是一个常量,和设备有关,在不同设备上这个值可能不同; 当手指在屏幕上滑动的距离小于这个值,系统就不会认为这是滑动操作; 获取方法: ViewConfiguration.get(getContext()).getScaledTouchSlop(); 这个常量的意义在于,当我们做滑动处理的时候,可以用它来做一些过滤。这样可以改善用户的使用体验。 VelocityTracker 速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。下面是使用示例: @Override public boolean onTouchEvent(MotionEvent event) { //创建VelocityTracker对象,并将触摸界面的滑动事件 //加入到VelocityTracker当中。 VelocityTracker velocityTracker = VelocityTracker.obtain(); velocityTracker.addMovement(event); //指定时间间隔为1s,计算速度 velocityTracker.computeCurrentVelocity(1000); //获取水平速度和竖直速度(像素/s) int xVelocity = (int)velocityTracker.getXVelocity(); int yVelocity = (int)velocityTracker.getYVelocity(); //销毁对象,回收内存 velocityTracker.clear(); velocityTracker.recycle(); return super.onTouchEvent(event); } 速度 = (终点位置 - 起点位置)/时间段(可以为负值) GestureDetector 手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。下面是使用示例: public class MainActivity extends AppCompatActivity implements GestureDetector.OnGestureListener,GestureDetector.OnDoubleTapListener{ GestureDetector mGestureDetector; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //首先,创建GestureDetector对象并根据需求实现OnGestureListener接口 //和OnDoubleTapListener接口 mGestureDetector = new GestureDetector(this,this); } @Override public boolean onTouchEvent(MotionEvent event) { //将目标View中的触摸事件交给GestureDetector处理(这里是MainActivity中的触摸事件) return mGestureDetector.onTouchEvent(event); } } 做完上面的事情之后,即可以选择性的实现OnGestureListener和OnDoubleTapListener中的方法了,这两个接口中的常用方法如下: 方法名 描述 所属接口 onDown 手指轻轻触摸屏幕瞬间,由一个ACTION_DOWN触发 OnGestureListener onSingleTapUp 手指(轻轻触摸屏幕后)松开,伴随一个ACTION_UP而触发,表示单击行为 OnGestureListener onScroll 手指按下屏幕并拖动,由一个ACTION_DOWN,多个ACTION_MOVE触发,表示拖动行为 OnGestureListener onFling 用户按下触摸屏,快速滑动后松开,由一个ACTION_DOWN、多个ACTION_MOVE和一个ACTION_UP触发,表示快速滑动行为 OnGestureListener onLoongPress 用户长久的按住屏幕不放,表示长按行为 OnGestureListener onDoubleTap 双击,由连续2次单击组成 OnDoubleTapListener 说明: 在实际开发中,可以不使用GestureDetector,完全可以自己在View中onTouchEvent方法中实现所需的监听。 建议:如果只是监听滑动相关,就在onTouchEvent中实现;如果要监听双击这种行为,就使用GestureDetector。
启动模式 在这里,首先要提到一个名词——任务栈(Task),数据结构中的栈我们都很熟悉,而Android系统采用栈的结构来管理应用程序运行过程中所启动的Activity,即任务栈。知道这一点,对于我们理解启动模式已经足够了。 standard(标准模式):系统的默认启动模式,每次启动一个Activity,都会重新创建一个实例,无论这个实例是否已经被创建。 通常,当我们使用显式Intent启动一个standard模式的Activity时: Intent intent = new Intent(MainActivity.this, NextActivty.class); startActivity(intent); NextActivity会进入MainActivity所在的栈中,即被启动的Activity会进入启动它的那个Activity所在的任务栈中。 这是一种典型的多实例模式,一个任务栈中可以有多个实例,每个实例也可以属于不同的任务栈。 注意:当我们用ApplicationContext启动一个standard模式的Activity时会报错,这是因为standard模式的Activity会默认进入启动它的Activity所属的任务栈中,但是非Activity类型的Context(如:ApplicationContext)没有所谓的任务栈。 singleTop(栈顶复用模式):在这种模式下,如果新Activity位于任务栈栈顶,就不会重新创建,而是回调它的 onNewIntent 方法;如果新Activity已经存在但不是位于栈顶,仍会重新创建新Activity。 例如:假设目前栈内情况为ABCD(D为栈顶),这时要再次启动D,如果D是singleTop模式,栈内情况不变;如果D是standard模式,栈内情况就会变为ABCDD。 singleTask(栈内复用模式):在这种模式下,只要Activity在一个栈中存在,多次启动这个Activity都不会重新创建实例,而是将这个Activity上面的Activity全部出栈,直到这个Activity位于栈顶;并且系统也会回调它的 onNewIntent 方法。 这是一种单实例模式,在一个任务栈中,一个Activity只会存在一个实例。 举例: 假设当前任务栈S1中的情况为ABCD,这时ActivityA以singleTask模式请求启动,其所需要的任务栈为S2,系统就会创建任务栈S2,然后创建A的实例入栈S2,任务栈S1不变。 假设A所需的任务栈为S1,其余情况同上,那么系统就会将BCD全部出栈,然后调用A的 onNewIntent 方法,这时任务栈S1内情况为A。 singleInstance(单实例模式):这种模式在singleTask模式的基础上,要求通过这种模式创建的Activity只能单独位于一个任务栈中,并且这个任务栈中只允许有一个实例存在。 上面是关于四种启动模式的概述,接下来再说明一些特殊情况: 假设目前有两个任务栈,前台任务栈的情况是AB,后台任务栈的情况是CD,假设CD的启动模式均为singleTask。现在请求启动C,由于后台任务栈中C位于栈底,所以C上面的Activity会被出栈,这时前台任务栈的情况为C,后台任务栈的情况为AB(过程见下图1.1)。 图1.1 如果上面请求启动D,那么整个后台任务栈会切换到前台,前台任务栈会被切换到后台(过程见下图1.2)。 图1.2 补充: 为Activity指定所需任务栈的方式: 在AndroidMenifest文件下,对应的activity标签内,有一个TaskAffinity属性,在默认情况下(即不指定TaskAffinity属性值),所有activity所需任务栈的名字为应用的包名。若我们要为某个Activity指定所需任务栈,只需指定TaskAffinity属性值和包名不同即可,这个值会作为这个Activity所在任务栈的名字,当然,这个Activity的启动模式为singleTask时才有意义。 <activity android:name=".SecondActivity" android:launchMode="singleTask" android:taskAffinity="com.example.laughter.task_1"/> 为Activity指定启动模式的方式: 在AndroidManifest文件中指定 launchMode 属性值。 <activity android:name=".SecondActivity" android:launchMode="singleTask" android:taskAffinity="com.example.laughter.task_1"/> 通过在Intent中设置标志位来指定启动模式。(优先级高于第一种,两种方式同时存在时,以第二种为准。) Intent intent = new Intent(MainActivity.this, SecondActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); Activity的常见Flags(标志位) Activity的Flags有很多,有的可以设定Activity的启动模式,有的可以影响Activity的运行状态,在使用时需要注意有些标志位是系统内部使用的,应用程序不需要去手动设置这些标志位以防出现问题。 FLAG_ACTIVITY_NEW_TASK 为Activity指定“singleTask”启动模式 FLAG_ACTIVITY_SINGLE_TOP 为Activity指定“singleTop”启动模式 FLAG_ACTIVITY_CLEAR_TOP 具有此标志位的Activity,当它启动时,在同一个任务栈中所有位于它上面的Activity都要出栈。若该Activity为standard启动模式,那么它自身也会出栈,系统会重新创建这个Activity的实例放入栈顶。根据前面的内容可知,singleTask启动模式默认具有此标记位的效果。
最近笔试常常遇到考察Java代码块执行顺序的题目,网上查看博客错漏百出,特地自己测试了一下。 如有错漏,希望路过的大佬指出来,以便我进行更改。 先上代码吧! public class ClassA { private static String a = print("A"); static { System.out.println(a+":A——静态块"); } { System.out.println("A——构造块"); } public ClassA() { System.out.println("A——默认构造方法"); } public ClassA(String name) { System.out.println("A——带参构造方法"); } public static String print(String className) { System.out.println(className+"——静态属性"); return className; } } public class ClassB extends ClassA{ private static String b = print("B"); static { System.out.println(b+":B——静态块"); } { System.out.println("B——构造块"); } public ClassB() { System.out.println("B——默认构造方法"); } public ClassB(String name) { System.out.println("B——带参构造方法"); } } public class ClassTest { public static void main(String[] args) { System.out.println("start"); ClassB laughter = new ClassB("laughter"); System.out.println("------------------"); ClassB Somnus = new ClassB(); } } 接下来是测试结果 总结 结合我查询的资料以及测试结果,可以得知执行顺序如下: main 函数作为入口,按行依次执行; 静态属性 > 静态方法声明 > 静态块; 动态属性 > 动态方法声明 > 构造块; 构造方法。 补充说明: 创建一个对象时,无论是否带参数,都会先调用它的父类(如果存在父类)的默认构造方法。
题目来自牛客网真题 传送门 1. 知识点:Java只能直接继承自一个父类,即只支持单重继承,而可以实现多个接口。 2. 知识点: Java创建线程有三种方式: 1.继承 Thread 类,重写 run() 方法; 2.实现 Runable 接口,重写 run() 方法; 3.使用Callable和Future接口创建线程。 参考:java 多线程总结(一) 创建线程的几种方法及对终止线程运行的讨论 线程间通信所使用的方法wait , notify , notifyAll 是Object类提供的方法: wait(): 将当前线程加入到等待队列中,等待其他线程调用notify或者notifyAll来唤醒。 notify(): 唤醒在等待队列中等待的单个线程. 如果多个线程都是在等待队列中等待, 则随机唤醒一个. notifyAll(): 唤醒所有在等待队列中等待的所有线程。 关于第二个选项后续补充 3. 知识点:onSaveInstanceState()方法只有在activity异常终止(如:屏幕旋转)时才会调用。当Activity异常终止时,系统会调用onSaveInstanceState()方法来保存当前Activity的状态(调用发生在onStop() 之前);当Activity被重建之后,系统调用onRestoreInstanceState()方法来恢复Activity的状态(调用发生在onStart()之后)。 4. 知识点:Activity被强制关闭(即异常情况下),系统会自动调用onSaveInstanceState()方法和onRestoreInstanceState()方法,保存和恢复UI状态。 5. 知识点:进程死锁:如果多个进程同时占有对方需要的资源而同时请求对方的资源,而它们在得到请求之前不会释放所占有的资源,那么就会导致死锁的发生,也就是进程不能实现同步。题目分析:要保证无论如何都不发生死锁,就要从极端情况考虑:当四个并发进程都占有了4个资源时,再加上一个资源,就能保证至少有一个进程可以获得5个资源,从而保证不会发生死锁。即:4*4+1=17个。 6. 参考博客:tcp关闭状态详解 盗了一张图 7. 题目分析:常规思路分析的话,可以从最后一个入栈元素入手,分以下情况讨论: 4××× 若最后一个元素第一个出栈,说明前面入栈的元素都没有出栈,所以只有一种出栈序列:4321。 ×4×× 若最后一个元素第二个出栈,说明前面入栈的元素有两个没有出栈,并且其出栈顺序固定,所以有三种出栈序列:1432,2431,3421。 ××4× 若最后一个元素第三个出栈,则其前面出栈的两个元素顺序不定,所以有6种出栈序列:1243,2143,1342,3142,2341,3241。 ×××4 若最后一个元素最后出栈,即最后一个元素对前三个元素的入栈和出栈没有任何影响,那么就可以用这个思路对 123 这个序列进行分析,将3作为最后一个入栈的元素,依次类推。有三种出栈序列:3214,2314,1234,2134。 特别说明:题目满足卡特兰数,可用公式 计算。即 = = 14。 8. 答案有争论,后续整理更新 9. 知识点:^ 会匹配行或者字符串的起始位置;[^] 表示不接受该字符集合; + 表示重复1次及以上(1~n); * 表示重复0次及以上(0~n); ? 表示重复0次或1次;题目分析:^d表示以 d 开头,d+ 表示d重复至少一次,[^d] +表示任意不含d的字符串。 10. 题目分析:第一次查找 (0+10)/2 = 5 下标为5的数:50<90; 第二次查找(6+10)/2=8 下标为8的数:90 。 11. 题目分析:根据前序遍历(中左右)序列可知,A为根节点,接下来看中序遍历(左中右)序列,则DBGE为左子树,CHF为右子书;接下来,看左子树的前序遍历序列:BDEG,所以左子树的根节点为B,那么左子树的左孩子和右子树分别为D和GE;接着看右子书的前序遍历序列:CFH,所以右子书的根节点为C,那么右子树的右子树为FH;以此类推即可得该二叉树。 12. 知识点:FileInputStream和FileOutputStream可以对文件进行读写;BufferReader和BufferWriter带缓存,也可以对文件进行读写。 13. 知识点:Last-Modified 标示这个响应资源的最后修改时间。 Etag web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识。 14. 知识点: between 关键字是一个闭区间。 &lt; < &lt;= <= &gt > &gt;= >= 15. 16. 知识点:shell函数在调用时直接写函数名,不带括号。 参考博客:shell中函数的定义和使用 17. 知识点: awk用法:awk 'BEGIN{执行前语句}{每一行执行的语句}END{所有行执行完后执行的语句}' 实例:累加每行第一列数字 awk 'BEGIN{sum=0}{sum+=$1}END{print sum}' 也可以省略BEGIN awk '{sum+=$1}END{print sum}' 18. 知识点: GET请求提交参数有长度限制,而POST没有长度限制; POST方式比GET安全,因为GET方式所发送的数据是URL的一部分,而POST参数不会被保存在浏览器历史或web服务器日志中。 19. 参考博客:Android事件拦截机制 20. 知识点:常用的布局优化方式有: 使用include标签实现布局重用,例如自定义的ToolBar; 使用merge标签减少不必要的嵌套,用于除去没有background等属性的外层FrameLayout; ViewStub标签实现延迟加载。 推荐一篇博客:一些你需要知道的布局优化技巧 21. 参考博客:ANR问题总结 22. 知识点: TCP是面向连接的,而UDP是无连接的; TCP提供可靠的服务,UDP尽最大努力交付,即不保证可靠交付; TCP只支持点对点,UDP支持一对一、一对多、多对多的交互通信; 23. 24. 题目分析: C选项表示从表中查找有课程成绩大于80的学生姓名(不是所有课程成绩大于80); D选项表示查找最低分数大于80的学科(将 sub_name 改为 stu_name 即为正确)。 25. 题目分析:对于这个题,只需要知道静态块最先执行就可以得出答案。测试博客:特地写了两个简单的类进行了测试
掌握了单链表的结构和实现方法后,再来看双向链表,其实就是在每个节点上添加一个指向其前驱节点的指针,这样就可以实现链表的双向遍历,提高了访问效率。 下面是几个方法的实现: 首先依旧是节点的结构 template<class T> struct Node{ T Data; Node<T>* Prior; Node<T>* Next; }; 插入函数 template<class T> void DouLinkList<T>::ele_insert(T data, int posi) { Node<T>* sign = Head; for(int i=1;i<posi;i++) { sign = sign->Next; if(sign == nullptr) throw"out of size"; } Node<T>* p = new Node<T>; p->Data = data; p->Next = sign->Next; p->Prior = sign; sign->Next->Prior = p; sign->Next = p; } 图1 插入节点 删除函数 template<class T> void DouLinkList<T>::ele_delete(int posi) { Node<T>* sign = Head; for(int i=0;i<posi;i++) { sign = sign->Next; if(sign == nullptr) throw"out of size"; } sign->Prior->Next = sign->Next; sign->Next->Prior = sign->Prior; delete sign; sign = nullptr; } 图2 删除节点 源码链接:DouLinkList.hDouLinkList.cpp 上一篇:数据结构与算法(二) 线性表之单链表 下一篇:数据结构与算法(四) 常用排序算法
在初学线性表的时候,感觉链表实现起来确实很绕,毕竟没有顺序表那么直观,不过熟悉时候,感觉也就是把一个一个节点连接起来,只要在纸上画一画,其实也是比较好理解的。 话不多少,直接上代码。 首先是节点的结构 template<class T> struct Node{ //数据 T Data; //指向下一个节点的指针 Node<T>* Next; }; 构造函数 template<class T> LinkList<T>::LinkList(T* data, int len) { Head = new Node<T>; Head->Next = nullptr; Node<T>* sign = Head; for(int i=0;i<len;i++) { Node<T>* p = new Node<T>; p->Data = data[i]; p->Next = nullptr; sign->Next = p; sign = p; } } 图1 构造过程 插入函数 template<class T> void LinkList<T>::ele_insert(T data, int posi) { Node<T>* sign = Head; for(int i=1;i<posi;i++) sign = sign->Next; Node<T>* p = new Node<T>; p->Data = data; p->Next = sign->Next; sign->Next = p; } 图2 插入过程 其实单链表比较不好理解的地方也就是向链表中插入节点,相信上面两幅草图与代码描述的还是比较清晰的。当然,功能函数远不止如此,根据实际需求添加即可。源码链接:LinkList.hLinkList.cpp 上一篇:数据结构与算法(一) 线性表之顺序表 下一篇:数据结构与算法(三) 线性表之双向链表
下文将分两部分来讨论Activity的生命周期,参照任玉刚老师的《Android开发艺术探索》一书,结合所学进行总结扩充。 典型情况下的生命周期 异常情况下的生命周期 典型情况下的生命周期 下图表示正常情况下Activity的生命周期过程。 image.png 下面是7个生命周期: /* onCreate: * 表示Activity正在被创建,执行一些初始化工作 * (如:调用 setContentView 加载界面布局资源,初始化Activity所需数据等); */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } /* onStart: * 表示Activity正在被启动,这时Activity已经可见(对应用可见), * 只不过还在后台,用户还看不到; */ @Override protected void onStart() { super.onStart(); } /* onResume: * 表示Activity已经出现在前台,用户可见; */ @Override protected void onResume() { super.onResume(); } /* onPause: * 表示Activity正在停止,可以执行存储数据、停止动画等不太耗时的工作 * (因为新活动的onCreate方法要在旧活动的onPause执行完才执行, * 为了不影响用户体验,要求这个过程尽可能在很短时间内完成); */ @Override protected void onPause() { super.onPause(); } /* onStop: * 表示Activity即将停止,可以做一些稍微重量级的工作,同样不能太耗时; */ @Override protected void onStop() { super.onStop(); } /* onDestory: * 表示Activity即将被销毁,执行回收工作和资源释放。 */ @Override protected void onDestroy() { super.onDestroy(); } /* onRestart: * 表示Activity正在重新启动, * 执行顺序为:onPause → onStop → onRestart → onStart → onResume */ @Override protected void onRestart() { super.onRestart(); } 上面描述的是Activity正常情况下的生命周期,这里再结合我们平常使用手机过程中的一些操作,具体说明一下其生命周期过程: 初次启动一个Activity,回调情况: 从Activity A 打开新的Activity B,B活动回调情况:onPause -> ( Activity B 启动)-> onStop 。这里还有一种特殊情况,当新启动的Activity采用透明主题时,当前Actvity不会回调onStop方法。(见补充说明1) 从A活动切换到B活动,再回到A活动,期间A活动没有调用finish()方法。A活动回调情况: 按back键退出Activity A,A活动回调情况:(见补充说明2) 息屏,然后亮屏,回调情况: 实践出真知,这些过程都可以自己写个简单的Demo验证一下,印象更深刻一些。 补充说明: onStart 和 onStop,onResume 和 onPause 这两个配对的回调是具有不同意义的,onStart 和 onStop 是从Activity是否可见这个角度来回调的,而onResume 和 onPause则是从Activity是否位于前台来回调的,在实际使用过程中没有其他明显区别。说明:onStart(后台可见)-> onResume(前台可见)-> onPause(后台可见)-> onStop(后台也不可见)。 个人观点:上述实例2的原因是在回调onStop方法前,Activity A在后台是可见的,只不过不在前台无法与用户发生交互,如果回调了onStop,后台的Activity A也不可见了,那么Activity B的透明背景之后是默认的白色背景,视觉上会显得十分尴尬,个人觉得这么设计也是为了优化用户体验吧。 默认情况下,按back键,会回调Activity的 onDestroy 方法,销毁当前实例。 从Activity A跳转到 Activity B的过程中,先回调 A 的onPause方法,然后创建Activity B,然后才回调 A 的 onStop 方法(在上面的2,3两个实例中都能发现),因此,不要在onPause方法中执行耗时的操作,以尽快启动Activity B,使得用户拥有流畅的使用体验。 异常情况下的生命周期 根据手机使用过程中的常见情形,我们从两种情况来讨论Activity在异常模式下的生命周期: 情况1: 资源相关的系统配置发生改变导致Activity被杀死并重新创建 最常见的情形就是手机屏幕发生旋转时,由于系统配置发生改变,在默认情况下(即没有特殊设置),Activity会被销毁并重新创建。其生命周期如下图: 与正常生命周期相比,多了数据的保存和恢复这两个过程。当Activity在异常情况下终止时,系统会调用onSaveInstanceState方法将Activity的状态保存为一个Bundle对象,这个对象会在Activity重新创建后传递给onRestoreInstanceState方法和onCreate方法,这个方法的调用时机是在onStop之前,与onPause没有既定的时序关系。当Activity被重新创建后,系统会调用onRestoreInstanceState,将onSaveInstanceState方法保存的Bundle对象作为参数,取出其中的数据进行恢复,这个方法的调用时机是在onStart之后。根据这一点,我们可以判断onRestoreInstanceState方法是否被调用或者onCreate方法中的Bundle参数是否为null来确定Activity是否被重建。 每个View都有自己的onSaveInstanceState方法和onRestoreInstanceState方法,以根据不同View的需求来恢复不同的数据,例如:TextView恢复了自身文本的选中状态和文本内容。 前面提到,onSaveInstanceState方法保存的数据会传递给onRestoreInstanceState方法和onCreate方法,也就是说,进行数据恢复时,有两种方式,一种是在onCreate方法中进行,一种是在onRestoreInstanceState方法中进行。但是在onCreate方法中进行数据恢复的话,需要考虑Activity是正常启动的还是被重建的,如果是正常启动,那么onCreate(Bundle onSaveInstanceState)中的onSaveInstanceState参数是null。当然,官方文档是建议采用onRestoreInstanceState方法来恢复数据的。 前面强调了在默认情况下,系统配置发生改变时,Activity会被重新创建,也就是说,这是可以改变的。我们知道,在AndroidManifest文件中会对每个Activity进行注册,而在Activity标签下有android:configChanges这个属性。这个属性下包含很多值,与一些系统配置相对应,当我们希望在某个系统配置改变时不重建这个Activity,就可以在configChanges属性中添加对应的值。 常用的有: 1.orientation:屏幕方向发生改变,比如手机屏幕旋转; 2.locale:设备的本地位置发生了改变,一般指切换了系统语言; 3.keyboardHidden:键盘的可访问性发生了改变,例如用户调出了键盘。 <activity android:name=".MainActivity" android:configChanges="orientation|locale|keyboardHidden"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> 补充: 1. 当我们指定了configChanges属性时,如果指定的系统配置发生改变,不会重建Activity,但是会调用Activity的 onConfigurationChanged 方法,我们可以根据自己的需求重写这个函数。 情况2:资源内存不足导致低优先级的Activity被杀死 这种情况不方便模拟,但生命周期和情况1是相同的。Activity的优先级由高到低如下: 1.前台Activity; 2.可见但非后台Activity——例如被Dialog遮挡的的Activity; 3.后台Activity——执行了onStop的Activity。 如果一个进程中没有四大组件在执行,那么这个进程将很快被杀死,因此,一些后台工作最好是放在Service中从而提高优先级,不至于轻易被系统杀死。 下一篇:Android笔记 | Activity的启动模式
线性表是一种最简单、最常用的数据结构,根据存储方式可以分为顺序表和链表。 顺序表: 顺序表指的是用一组地址连续的存储单元依次存储线性表的数据元素,称为线性表的顺序存储结构或顺序映像(sequential mapping)。它以“物理位置相邻”来表示线性表中数据元素间的逻辑关系,可随机存取表中任一元素。 链表:链表指的是用一组任意的存储单元存储线性表中的数据元素,称为线性表的链式存储结构。它的存储单元可以是连续的,也可以是不连续的。在表示数据元素之间的逻辑关系时,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置),这两部分信息组成数据元素的存储映像,称为结点(node)。 接下来就是具体实现了(C++) 这里是 SeqList 这个类的成员变量 //数据集合 T *Data; //当前顺序表中元素数量 int Length; //顺序表容量 int Size; 首先是构造函数 template<class T> SeqList<T>::SeqList(T *Data, int Length, int Size) { this->Data = Data; this->Size = Size; this->Length = Length; this->Data = new T[Size+1]; for(int i=0;i<Length;i++) this->Data[i+1] = Data[i]; } 接下来是几个主要的成员方法 插入函数 向指定位置插入一个元素,由于顺序表是顺序存储的,所以需要考虑所给位置是否超过当前顺序表长度,插在末尾即为(length+1),所以未知参数在[1,Length+1]范围内才能进行插入操作。这时,由于顺序表是以数组作为存储结构,还要考虑在插入一个元素之后会不会发生数组越界,即(Length+1<Size)时,才能进行插入,否则要进行扩容,这里每次长度不够时长度增加10,即创建一个新的数组,容量为(Size+10)。 template<class T> void SeqList<T>::ele_insert(int loca,T data) { if(loca>Length+1 || loca<1) throw "out of size"; if(Length+1>=Size) { T* p = Data; Data = new T[Size+10]; for(int i=1;i<=Length;i++) Data[i] = p[i]; delete []p; p = nullptr; Size+=10; } for(int i=Length+1;i>loca;i--) Data[i] = Data[i-1]; Data[loca] = data; Length++; } 删除函数 删除指定位置的元素。思路就是依次前移将指定位置的元素覆盖掉,直接上代码。 template<class T> void SeqList<T>::ele_delete(int loca) { if(loca<1 || loca>Length) throw"out of size"; for(int i=loca;i<Length;i++) Data[i] = Data[i+1]; Length--; } 查询函数 查询顺序表中是否存在某个元素,存在则返回第一次出现位置的下标(下标从1开始),否则返回-1,简单的遍历。 template<class T> int SeqList<T>::ele_locate(T data) { for(int i=1;i<=Length;i++) { if(Data[i] == data) return i; } return -1; } 这里所列出的只是几个常用功能的实现,具体可以根据自己的需求进行扩充,放个源码链接SeqList.hSeqList.cpp 下一篇:数据结构与算法(二) 线性表之单链表
我们知道,String对象是不可变的,而Java中String类提供了“+”进行字符串拼接操作,从JDK1.5开始,字符串的拼接操作是通过StringBuffer类来完成的。 String a = "str"; String b = "ing"; String c = a + b; 上述代码的实际实现过程是: String c = new StringBuffer(a).append(b).toString(); 也就是说,在这个过程中实际创建了一个StringBuffer对象和一个String对象。因此,当对字符串进行修改的时候,使用 StringBuffer 和 StringBuilder 类,系统开销比较小。 与 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。 区别 String 不可变,而 StringBuffer 和 StringBuilder 是可变的。 StringBuffer 是线程安全的,内部使用 synchronized进行同步,而 StringBuilder 不是线程安全的。 根据这些主要区别: 进行字符串拼接操作时,使用StingBuffer 和 StringBuilder 可以节省系统开销。 在要求线程安全的情况下,应该使用 StringBuffer。
String类源码 public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** The offset is the first index of the storage that is used. */ private final int offset; /** The count is the number of characters in the String. */ private final int count; /** Cache the hash code for the string */ private int hash; // Default to 0 /** use serialVersionUID from JDK 1.0.2 for interoperability */ private static final long serialVersionUID = -6849794470754667710L; ........ } 可以看出: String 被声明为 final,因此不可被继承。并且成员方法都默认为 final 方法。 内部使用 char 数组存储数据,该数组也被声明为 final,也就是说 String 初始化之后就不能再引用其他值,并且其内部方法均不会改变它的值,这就确保了 String 不可变。 String Pool (字符串常量池) JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用String Pool。当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串。 String 的创建 String主要有两种创建方式: //第一种 String str1 = "hello world"; //第二种 String str2 = new String("hello world"); System.out.println(str1 == str2); //false 对于第一种创建方式,JVM会首先在String Pool中查找是否存在该对象,若存在则返回该对象的引用;否则,在String Pool中创建该对象,并返回该对象的引用。 对于第二种创建方式,JVM同样首先在String Pool中查找是否存在该对象,若不存在,则在String Pool 中创建该对象,并且在堆中创建一个对象,然后将堆中对象的引用返回给用户;否则,直接在堆中创建一个String对象,返回该对象引用给用户。 String str1 = new String("hello world"); String str2 = new String("hello world"); System.out.println(str1 == str2); //false 以上代码创建了 3个 “hello world” 对象,String Pool 中一个,heap(堆)中两个。 String str1 = "hello world"; String str2 = "hello world"; System.out.println(str1 == str2); //true 以上代码仅仅在 String Pool 中创建了一个 “hello world” 对象。 本文参考博客:深入理解Java中的StringString和String Pool的解析
8个基本数据类型 boolean(1) byte(8) char(16) short(16) int (32) long(64) float(32) double(64) 每种基本数据类型都有对应的包装器类型,基本数据类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。 简单来说 装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。 Integer i = 10; //装箱 int n = i; //拆箱 关于装箱与拆箱相关概念可参考博客 深入剖析Java中的装箱与拆箱 装箱过程是通过调用包装器的 valueOf 方法实现的,拆箱过程是通过调用包装器的 ×××value(×××表示对应的基本数据类型)方法实现的。 // 自动装箱,与下方调用 valueOf方法等同 Integer i1 = 200; Integer i2 = 200; System.out.println(i1 == i2); // false Integer i3 = Integer.valueOf(123); Integer i4 = Integer.valueOf(123); System.out.println(i3 == i4); // true Integer i5 = new Integer(200); Integer i6 = new Integer(200); System.out.println(i6 == i5); // false new Integer() 与 Integer.valueOf() 的区别在于,new Intege() 每次都会创建一个新的对象,而 Integer.valueOf() 可能会使用缓存对象,这涉及到缓存池,Integer的缓存池大小默认为-128~127,当数值在 [-128,127] 时,便返回指向Integer缓存池 (IntegerCache.cache )中已经存在的对象的引用;否则,创建一个新的Integer对象。 i1 和 i2 数值为200,会创建新的对象,因此不等 i3 和 i4 数值为123 ,指向同一个对象,因此相等 i5 和 i6 都是直接创建新的对象,因此不等