Integerを"=="で比較してはいけない
追記:Javaの話です。
public class Main { public static void main(String args[]) throws Exception { Integer i1 = 1; Integer i2 = get1(); if (i1 == i2) { System.out.println("equal"); } else { System.out.println("not equal!!"); } } public static Integer get1() { // DBとかから取ってきたつもり return new Integer(1); } }
は、"not equal!!"と言われてしまいます。
(ただし、コンパイラや実行環境に依存するかもしれません。)
Integerはプリミティブ型ではないので、==
では、同一のインスタンスかどうかを確認してしまっているんですね。(たぶん
普通、比較といえば、値の同一性を知りたいと思うので、
public class Main { public static void main(String args[]) throws Exception { Integer i1 = 1; Integer i2 = get1(); if (i1.equals(i2)) { // ←ここを変えた System.out.println("equal!!"); } else { System.out.println("not equal!!"); } } public static Integer get1() { // DBとかから取ってきたつもり return new Integer(1); } }
まぁ、こうしますよね。
ただ、上記の場合は i1
がnullの場合はNullPointerExceptionが発生します。当たり前ですね。
なので、 i1
が必ずnullでは無い場合にしか使えないです。
nullの場合、一致とみなすか、不一致とみなすかは文脈によるかと思いますので、割愛します。
org.apache.commons.lang.ObjectUtils.equals
を利用するのも手ですね。
8/10追記 java.util.Objects.equals ってのがJava7からあるみたいです。(コメントで教えていただきました。)
ちゃんと考えれば当たり前なんですが、誰かの書いたコードで、ぼんやり読んでるとハマりますね。(というか、ハマりました。
androidからGAEのサーバにデータを送信する
androidアプリと、GAE(Python)の連携です。
共円チェッカーでステージを送信するときのコードです。
String url = context.getString(R.string.upload_url); HttpClient httpClient = new DefaultHttpClient(); HttpPost httpPost = new HttpPost(url); List<NameValuePair> nameValuePair = new ArrayList<NameValuePair>(); nameValuePair.add(new BasicNameValuePair("data", params[0])); try { httpPost.setEntity(new UrlEncodedFormEntity(nameValuePair, HTTP.UTF_8)); HttpResponse response = httpClient.execute(httpPost); String ret = EntityUtils.toString(response.getEntity()); return ret; } catch (IOException e) { throw new RuntimeException(e); } finally { httpClient.getConnectionManager().shutdown(); }
URLはstrings.xmlに定義し、Context#getString(int)にて取得しています。
データの送信には、org.apache.http.client.HttpClientを利用しました。
また、GETではなく、POSTで送信するために、org.apache.http.client.methods.HttpPostを利用しています。
で、送信したいデータはparams[0]に設定してあるので、BasicNameValuePairに設定し、List
HttpEntityEnclosingRequestBase#setEntity(HttpEntity)に設定する際にUTF-8でエンコードしています。(たぶん、この設定のおかげで日本語が送信できています)
設定が完了したら、HttpClient#execute(HttpUriRequest)でデータを送信します。
EntityUtils#toString(HttpEntity)で、レスポンスを文字列として取得しています。
あとは、finally節にてHttpClientのクローズを行っています。
で、これを受け取るサーバ側
#!/usr/bin/env python #-*- coding: utf-8 -*- # import logging from google.appengine.ext import webapp, db from google.appengine.ext.webapp.util import run_wsgi_app # パズルのステージ情報 class KyouenPuzzle(db.Model): # ステージ番号 stageNo = db.IntegerProperty(required=True) # サイズ size = db.IntegerProperty(required=True) # ステージ上の石の配置 stage = db.StringProperty(required=True) # 作者 creator = db.StringProperty() # 中略 # 登録処理 class KyouenRegist(webapp.RequestHandler): # POSTリクエストを処理します。 def post(self): # パラメータ名:dataを取得 data = self.request.get('data').split(',') logging.debug("post data:" + str(data)) # 入力データの登録 model = KyouenPuzzle(stageNo=self.getNextStageNo(), size=int(data[0]), stage=data[1], creator=data[2].replace('\n', '')) model.put() # レスポンスの返却 self.response.headers['Content-Type'] = 'text/plain' self.response.out.write('success stageNo=' + str(model.stageNo)) return application = webapp.WSGIApplication([('/regist', KyouenRegist) ], debug=True) def main(): run_wsgi_app(application) if __name__ == "__main__": main()
KyouenPuzzleクラスがDB登録用のクラスです。
4つのプロパティを持っています。で、それが登録されます。
KyouenRegistクラスは、javaで言う、javax.servlet.http.HttpServletを継承したクラス。
postメソッドを定義し、POSTリクエストを処理します。
リクエストデータをself.request.get('data')で取得できます。('data'はjavaで指定した文字列)
日本語としてちゃんと取得できました。
KyouenPuzzleのコンストラクタに対して、creatorに代入する際にreplace('\n', '')としているのは、エラー対策です。(creatorがmultiline=Trueとして定義されていないので、複数行のデータを登録するとエラーになります)
こんな感じでGAEにデプロイしておけば、android側で
<string name="upload_url">http://[設定したID].appspot.com/regist</string>
と設定しておけば、GAE上のDBにデータを登録できます。
Pythonは初めてだったので、ループの構文やら、文字列操作やらに手間取りましたが、簡潔に記述できて楽しかったです。
暇ができれば、ちゃんと本買って勉強しようと思います。
戻るボタンの制御について
色揃えるやつでは、パズル画面より戻った際に、リストに"CLEARED"と表示されます。
あと、クリア後に"Retry"ボタンが存在します。
このため、初回クリア後、"Retry"が押され、そのまま戻るボタンを押されると、リストの再描画のタイミングが無く、"CLEARED"が表示されませんでした。
そこで、戻るボタンを押下した際に、呼び出し元Activityに結果を返却することにしました。
まず、呼び出す際に、Activity#startActivityForResultにて呼び出すように変更。
で、呼び出し元ActivityにonActivityResultメソッドを実装。その中で、受け取ったクリア情報より内部保持しているリストを更新し、ArrayAdapter#notifyDataSetChangedを呼び出し。
あと、呼び出される側に下記の処理を追加。
@Override public boolean dispatchKeyEvent(KeyEvent e) { if (e.getKeyCode() == KeyEvent.KEYCODE_BACK) { if (e.getAction() == KeyEvent.ACTION_DOWN) { // 戻るボタンが押された場合 // 結果を設定 Intent intent = new Intent(); intent.putExtra("item", item); setResult(RESULT_OK, intent); finish(); return true; } } return super.dispatchKeyEvent(e); }
戻るボタンを押された際には、現在のステージ情報を必ず返すようにしました。
戻り値についてですが、dispatchKeyEventメソッドのJavaDocには下記のように記載されています。
Return true if this event was consumed.
エキサイト翻訳によると、
この出来事が消費されたなら、本当に戻ってください。
と、いうことなので、この場合はtrueを返すのが正解っぽいです。
ちなみに、"KeyEvent.ACTION_DOWN"ではなく、"KeyEvent.ACTION_UP"としてしまうと、2画面戻るといった、よくわからない動きをしてしまいます。
0.5から、0.5.2へバージョンアップしたのは、これを修正しました。
通知領域に文字を表示する
今回作成したサンプルプログラムです。
http://dl.dropbox.com/u/8518065/android/notificationSample1/SampleAndroid.apk
画像は、notifyボタンを押した直後の状態
で、下記がソース。
まずは、main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <EditText android:id="@+id/title" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="sample_title" /> <EditText android:id="@+id/message" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="sample_message" /> <Button android:id="@+id/button1" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="notify" /> </LinearLayout>
次に、メインとなるアクティビティクラス。
package hm.orz.chaos114.android.sample; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.EditText; public class SampleAndroidActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); init(); } private void init() { // タイトル入力領域を取得 final EditText titleTextView = (EditText) findViewById(R.id.title); // メッセージ入力領域を取得 final EditText messageTextView = (EditText) findViewById(R.id.message); // 通知ボタンを取得 Button button1 = (Button) findViewById(R.id.button1); button1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { CharSequence title = titleTextView.getText(); CharSequence message = messageTextView.getText(); NotificationUtil.notify(SampleAndroidActivity.this, title, message); } }); } }
あと、そこから呼ばれているユーティリティクラス。
package hm.orz.chaos114.android.sample; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; public class NotificationUtil { public static void notify(Context context, CharSequence title, CharSequence message) { // 通知をタップされたときのintentを作成 Intent newIntent = new Intent(context, SampleAndroidActivity.class); PendingIntent contentIntent = PendingIntent.getActivity(context, 0, newIntent, 0); // Notificationオブジェクトの作成 Notification notification = new Notification(R.drawable.icon, title, System.currentTimeMillis()); notification.setLatestEventInfo(context, title, message, contentIntent); notification.flags = Notification.FLAG_AUTO_CANCEL; // Managerオブジェクトの取得 NotificationManager notificationManager = (NotificationManager) context .getSystemService(Context.NOTIFICATION_SERVICE); // このアプリの通知を一旦削除し、再通知 notificationManager.cancelAll(); notificationManager.notify(0, notification); } }
あと、補足事項。
- NotificationManager#notifyの第一引数が、今回固定値となっています。
アプリケーション内で同一のidが指定された場合、cancelAllをしてやらないと、再通知されませんでした。
複数の通知を表示したいは、表示した最後の番号をSharedPreferencesとかに入れといて、それにインクリメントして表示するとか?
さらに、あとでcancel(通知を削除)したい場合、通知内容とidを保存しないといけないかな?(あまり、そんな場面が思いつきませんが。)
- 通知をクリックした際のintent
今回、SampleAndroidActivityを起動しています。
この場合、SampleAndroidActivityを起動したまま通知をタップすると、2枚同じアクティビティが起動してしまいます。(=戻るを押下しても、もう1個のSampleAndroidActivityが表示される)
これを解消する場合、newIntentを生成後に下記のように設定。
newIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
ただし、複数のアクティビティが定義されていたりした場合、意図しない動きをするかもしれません。
- PendingIntent
通知がタップされた際に発行するIntentを保持するIntentのようです。
あまりよくわかっていません。
javaによる2次元座標の計算など
とりあえず、下記に共円チェッカーで利用しているクラスの全文を記載します。
2011/07/18 22:30 Lineのコンストラクタ内の数式が誤っていたため、修正。また、参考サイトを記載。
package hm.orz.chaos114.android.kyouenchecker; import java.util.ArrayList; import java.util.List; public class GameModel { class Point { double x; double y; Point(double x, double y) { this.x = x; this.y = y; } double getAbs() { return Math.sqrt(x * x + y * y); } Point difference(Point p2) { return new Point(x - p2.x, y - p2.y); } Point sum(Point p2) { return new Point(x + p2.x, y + p2.y); } @Override public String toString() { return "Point [x=" + x + ", y=" + y + "]"; } } /** * Ax+By+C=0を表現するクラス。 */ class Line { Point p1; Point p2; double a; double b; double c; Line(Point p1, Point p2) { this.p1 = p1; this.p2 = p2; a = p1.y - p2.y; b = p2.x - p1.x; c = p1.x * p2.y - p2.x * p1.y; } double getY(double x) { double y = -1 * (a * x + c) / b; return y; } @Override public String toString() { return "Line [p1=" + p1 + ", p2=" + p2 + ", a=" + a + ", b=" + b + ", c=" + c + "]"; } } /** * 共円情報を表現するクラス。 * * @author noboru */ class KyouenData { Point p1; Point p2; Point p3; Point p4; public KyouenData(Point p1, Point p2, Point p3, Point p4) { this.p1 = p1; this.p2 = p2; this.p3 = p3; this.p4 = p4; } } List<Point> points = new ArrayList<Point>(); public void add(int x, int y) { Point p = new Point(x, y); points.add(p); } public KyouenData isKyouen() { if (points.size() < 4) { return null; } Point p1 = points.get(points.size() - 1); for (int i = 0; i < points.size() - 1; i++) { Point p2 = points.get(i); for (int j = i + 1; j < points.size() - 1; j++) { Point p3 = points.get(j); for (int k = j + 1; k < points.size() - 1; k++) { Point p4 = points.get(k); boolean kyouen = isKyouen(p1, p2, p3, p4); if (kyouen) { return new KyouenData(p1, p2, p3, p4); } } } } return null; } public boolean isKyouen(Point p1, Point p2, Point p3, Point p4) { // p1,p2の垂直二等分線を求める Line l12 = getMidperpendicular(p1, p2); // p2,p3の垂直二等分線を求める Line l23 = getMidperpendicular(p2, p3); // 交点を求める Point intersection123 = getIntersection(l12, l23); if (intersection123 == null) { // p1,p2,p3が直線上に存在する場合 Line l34 = getMidperpendicular(p3, p4); Point intersection234 = getIntersection(l23, l34); if (intersection234 == null) { // p2,p3,p4が直線状に存在する場合 return true; } } else { double dist1 = getDistance(p1, intersection123); double dist2 = getDistance(p4, intersection123); if (Math.abs(dist1 - dist2) < 0.0000001) { return true; } } return false; } /** * 2点間の距離を求める。 * * @param p1 座標1 * @param p2 座標2 * @return 距離 */ public double getDistance(Point p1, Point p2) { Point dist = p1.difference(p2); return dist.getAbs(); } /** * 2直線の交点を求める。 * * @param l1 直線1 * @param l2 直線2 * @return 交点座標(交点が存在しない場合、null) */ public Point getIntersection(Line l1, Line l2) { double f1 = l1.p2.x - l1.p1.x; double g1 = l1.p2.y - l1.p1.y; double f2 = l2.p2.x - l2.p1.x; double g2 = l2.p2.y - l2.p1.y; double det = f2 * g1 - f1 * g2; if (det == 0) { return null; } double dx = l2.p1.x - l1.p1.x; double dy = l2.p1.y - l1.p1.y; double t1 = (f2 * dy - g2 * dx) / det; return new Point(l1.p1.x + f1 * t1, l1.p1.y + g1 * t1); } /** * 2点の垂直二等分線を求める。 * * @param p1 座標1 * @param p2 座標2 * @return 垂直二等分線 */ public Line getMidperpendicular(Point p1, Point p2) { Point midpoint = getMidpoint(p1, p2); Point dif = p1.difference(p2); Point gradient = new Point(dif.y, -1 * dif.x); Line midperpendicular = new Line(midpoint, midpoint.sum(gradient)); return midperpendicular; } /** * 中点を求める。 * * @param p1 座標1 * @param p2 座標2 * @return 中点座標 */ public Point getMidpoint(Point p1, Point p2) { Point midpoint = new Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2); return midpoint; } }
で、説明など。
Pointクラス
2次元座標(x座標,y座標)を表現するためのクラスです。
ベクトル(x方向成分,y方向成分)も表します。
getAbsメソッド
座標の原点からの距離を返します。
ベクトルの大きさを表します。
differenceメソッド
sumメソッド
それぞれ、保持している座標と引数の座標の和・差を返します。
Lineクラス
Ax+By+C=0を表現するクラスです。
コンストラクタ
引数として、2つの座標を受け取ります。
保持する情報は、その2点を通過する直線の式となります。
下記サイトに書いてある通りにしました。
http://bobobobo.wordpress.com/2008/01/07/solving-linear-equations-ax-by-c-0/
getYメソッド
引数としてx座標を受け、保持している直線上のy座標を返します。
KyouenDataクラス
呼び出し元に共円となった4点を返したいがために定義したクラスです。
List
isKyouenメソッド
4点が登録されていない場合、判定を行いません。
最後に登録した点を基準とします。
それ以外の既存の3点を順に選んでゆき、それら4点が共円かどうかを判定します。
実際の判定処理は次のisKyouen(Point, Point, Point, Point)で行います。
isKyouen(Point, Point, Point, Point)メソッド
実際の共円判定を行います。
引数の4点が円周上に存在する場合、trueを返します。
以下、手順です。
- 点1、点2の垂直二等分線を求めます。以降、線12とします。
- 同じく、点2、点3の垂直二等分線を求めます。以降、線23とします。
- 線12、線23の交点を求めます。(=点1、2、3を通る円の中心)
- 線12、線23が平行な場合、nullが返却されます。(=点1,2,3が直線上に存在する)
- 点3、点4の垂直二等分線を求めます。以降、線34とします。
- 線23、線34の交点を求め、取得できない(=平行)な場合、点1,2,3,4が直線上に並んでいることになるため、trueを返します。
- 線12、線23が平行でない場合、交点が返却されます。以降、中点123とします。
- 点1と、中点123の距離を距離1とします。
- 点4と、中点123の距離を距離2とします。
- 距離1、距離2の差の絶対値が、10^-7以下の場合は同一とみなし、trueを返します。(10^-7なのは、特に理由はありません)
- 線12、線23が平行な場合、nullが返却されます。(=点1,2,3が直線上に存在する)
getDistanceメソッド
引数の2ベクトルの差を求めます。
そのベクトルの大きさを返します。
getIntersectionメソッド
引数の2直線の交点を求めます。
下記のURLの計算を実行しました。
http://www.h4.dion.ne.jp/~zero1341/t/03.htm
あまり、意味がわかってないです。
getMidperpendicularメソッド
引数の点1、点2の中点を求めます。中点12とします。
点1、点2のベクトルの差を求めます。ベクトル12とします。
ベクトル12のy成分に-1を掛け、x,yを逆にしたベクトルをベクトル21とします。
中点12を通り、(中点12にベクトル21を足した点)を通る線を返します。
getMidpointメソッド
引数の2点のx,y座標の、それぞれの和を2で割った値を設定したPointを返します。
と、いった形でチェックしています。
計算が誤っている場合など、コメント下さい。
共円チェッカーをAndroidマーケットに公開しました。
「共円チェッカー」なるアプリを公開しました。
http://market.android.com/details?id=hm.orz.chaos114.android.kyouenchecker
昨日の夜、「たけしのコマ大数学科」にて紹介されていた、「共円」を判定するアプリです。
共円については、以下のページに解説があります。
灘校数学研究部のホームページ
判定アルゴリズムは
3点を通る円の中心を(2本の垂直二等分線の交点として)求め、
その中心から残りの1点までの距離が
他の3点までの距離と等しいかどうかを調べます。
等しければ4点は共円であり、異なれば4点は共円ではありません。
を利用しています。
久々に数学してみたため、間違いなどあるかもしれません。
番組内で、アプリにする話が出てましたが、
先に(たぶん)作ってみました。
ただし、インターフェース(見た目)はひんそーです。
免責事項的なこと。(なんか違うような気もするけど)
noboru@「共円チェッカー」の作者(=suzaku114@このブログを書いている者)は、灘校数学研究部様とは関係がありません。
アプリに対するご意見・ご感想は灘校数学研究部ではなく、noboru@アプリの作者にお願いします。
HTML表示でハマった
基本的には、下記のページにあるとおりでできます。
http://d.hatena.ne.jp/androidprogram/20100518/1274184600
で、その取得してきたCharSequenceに対して、文字列を追加したくなり、下記のように記述しました。
strings.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="hoge">画像を <img src=\"image_%1$d\"> 表示。</string> </resources>
javaのソース
TextView textView = (TextView) findViewById(R.id.fuga); String text = String.format(getResources().getText(R.string.hoge) .toString(), 0); CharSequence html = Html.fromHtml(text, new Html.ImageGetter() { @Override public Drawable getDrawable(String source) { // 画像のリソースIDを取得 int id = getResources().getIdentifier(source, "drawable", getPackageName()); // リソースIDから Drawable のインスタンスを取得 Drawable d = getResources().getDrawable(id); d.setBounds(0, 0, 24, 38); return d; } }, null); textView.setText(html + "追加");
で、出てきたのが、画像部分が点線で囲まれた中に「OBJ」と書かれているアイコン。
ちなみに、drawableフォルダには「image_0.png」っていうファイルが、ちゃんとあります。
実際、htmlのクラスは「android.text.SpannableStringBuilder」なのですが、
「html + "追加"」のクラスは「java.lang.String」となってしまう。
そのへんが原因?
まぁ、当たり前ですが、ハマったのでメモ。
Amazon Product Adveristing APIにて、403エラー
以下のサンプルコードを元に、実装してみた。
http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html?AuthJavaSampleSig2.html
結果、はIOException。
表示されたメッセージは以下のような感じ。
Server returned HTTP response code: 403
いろいろ検索してみた結果、下記のサイトにて対処法を発見。
http://d.hatena.ne.jp/tanamon/20091212/1260640371
Base64 encoder = new Base64();
↑を、↓に変更。
Base64 encoder = new Base64(0);
ちなみに、Base64は「commons-codec-1.4.jar」に入っていました。
別のバージョンだったら、また違うのかもしれません。
BigDecimalとdoubleの変換について
doubleは浮動小数点型なので(?)、小数を正確に表現できないことは知ってました。
で、BigDecimalに変換する際に失敗しました。
以下、やっちゃった間違い。
double d = 0.1; BigDecimal b = new BigDecimal(d); System.out.println(b);
上記の出力結果
0.1000000000000000055511151231257827021181583404541015625
で、0.1を保持するBigDecimalオブジェクトを作成したい場合、
"0.1"という文字列よりBigDecimalを作成する必要があるらしい。
それを簡単にやってくれるのが、BigDecimal#valueOfメソッドだそうで、
double d = 0.1; BigDecimal b = BigDecimal.valueOf(d); System.out.println(b);
上記の出力結果
0.1
と、なりました。
大規模開発における、NoSuchMethodException
NoSuchMethodErrorだったかも。
今日(昨日?)あったこと。
現在、外部結合試験(?)中のソースを修正することになりました。
修正内容としては、JavaBeans?(ValueObjectとかDTOとか呼ばれる、Getter,Setterを持つクラス)に、
インスタンスフィールドを追加し、Getter,Setterも追加する。
んで、EJB内でDB読んで、Setterで値を入れて、条件によっては値を変更して、Getterで読みにいって、DBに再度書く。
フツーに、修正して、単体テストして、内部結合環境で疎通してもらって、外部結合環境にリリースしてもらいました。
で、その環境でテストしてもらったところ、NoSuchMethodExceptionが発生。
Setterが見つからないらしい。
でも、コンパイルは通ったらしい。
・・・?
意味不明。で、リリースが正しく行われているかとか、それ以前にちゃんと修正したものを提出しているかとか、いろいろ調べたけど、ちゃんとリリースされているらしい。
諦めきれずに、実際に環境にデプロイされているEARファイルを頂いてきて、解凍。
んで、出てきたJARを全部Eclipseに入れて、ビルドパスに通しました。
デコンパイラが既に入っていたので、修正したクラスを逆コンパイルしても、ちゃんと修正された物が入ってる。
ちゃんとリリースされていることが確認できたわけなんだけど、例外の原因が不明。
で、感のいい人ならここまでしなくても分かったのかもしれないけど、ここでようやく気づく。
Ctrl+Shift+Tで、クラス検索。
修正したBeanを検索したところ、2つある。
片方は自分が修正したもので、追加したGetter,Setterがちゃんと存在している。
で、もう片方は、追加する前の状態。
ナニコレ?
で、思い出したのが、このクラス、外部の部品とのインターフェースになっておりました。
そのため、その部品をコンパイルするためにそのBeanが必要。
だからコピーされてたらしい。(ってか、知らされてないし
で、クラスロードの際に、その部品内のBeanの方が優先され、コンパイルは通るけど、メソッドが見つからないという状態。
無事原因がわかってめでたしめでたし。
※ただし、対応方法が、「外部の部品の方にも同様のGetter,Setterを追加する」でした。
なんだか腑に落ちない。
まずもって、なんで部品のインターフェースオブジェクトが、部品を呼ぶ側に定義されてるんだろう・・・
まぁ、それを提案されて、却下出来なかった私も悪いんだけど・・・