私は 5 年以上 PHP 開発に携わってきたバックエンドエンジニアです。
昨年から1年程度 Java + Spring Framework で開発する案件に携わっておりましたので、そこで感じた言語や開発環境の差についてつらつらっと書きなぐってみました。

PHP は楽だけどここがあんまりだよね、Java っていいよね というスタンスです。

ワカテの PHP 技術者さんに見てもらいたいです。
理解が未熟な点はどうぞご指摘をお願いします。

言語の概要

PHPとは

PHP は LL と呼ばれる軽量なスクリプト言語です。

テンプレートエンジンとして生まれ機能が強化されてきました。
そのため、Web に特化にしており、最初に覚えることは比較的少ないと言えると思います。

また、過去の実績からまだまだ「セキュリティに弱い言語」というイメージが払しょくできていないのではないでしょうか。

また、「 とりあえず何でも配列に突っ込む 」など、簡単に悪い実装ができるため、設計や実装ルールをしっかり決めておくと、皆が幸せになれます。

Javaとは

Java は守備範囲が広い汎用的な言語です。

プログラムをコンパイルしてバイトコードを生成し、JavaVM 上で動作させます。

堅牢で信頼性が高く、シビアな要求がされるシステムで活躍する言語ですね。

メモリ消費が激しいとか、初回ロードに時間がかかってしまうイメージがありましたが、バージョンを追うごとに改善されていますね。

JREJDKJavaSEJavaEEなど、同じような単語がいくつも出てきて「どれを DL すればいいの?」と混乱したり、クラスパスに戸惑ったりと、初めに覚えることが多く、初心者に優しくない部類の言語だと思います。

また、ネット上にもJava入門がたくさんありますが、20 周年を迎えたということもあり、検索で上位にヒットするサイトは古さを感じる大御所が多いような気がします。

Webサーバで実行する

PHP

コンパイルが不要で、.phpファイルをWebサーバ上にアップロードすればとりあえず動くという手軽さ。

Web サーバは「 Apache+PHP 」や「 nginx+PHP-FPM 」といった構成のサーバをあらかじめ用意しておきます。

「 PHP をプログラムするエンジニア」と「” PHP を動作させるサーバ”を構築するエンジニア」が別担当者ということも珍しくなく、プログラム担当がサーバ側を全く知らないことも結構ある気がします。

Java

プログラムをパッケージ化Tomcatなどサーブレットコンテナ上に配置します。
デフォルトでは$CATALINA_HOME/webappsですね。

XML ファイルがいくつもあり、その複雑さに心が折れます。

依存ライブラリの管理

PHP

PHPの代表的なライブラリといえばPEARでしたが、昨今ではComposerが広く利用されるようになりました。

Composer
https://getcomposer.org/

Ruby には bundler というパッケージ管理ツールがありますが、Composer はその PHP 版ですね。

composer.jsonにライブラリを追加してコマンドを打つとcomposerが自動でライブラリをDLしてオートロードを生成してくれます。

Composer は依存ライブラリをプロジェクト内部に DL し、そのプロジェクトだけで利用できるようにします。

PHP の include_path に乗せるわけではありませんので、同じマシン内に複数バージョンのプロジェクトを並べることができます。

Java

私の参加したJavaプロジェクトではMavenを使用していました。

Apache Maven Project
https://maven.apache.org/

Maven は依存ライブラリの管理、ビルド、再配布までを管理するというプロジェクト管理ツールですので、「パッケージ管理」という単語だと語弊がありますね。

Maven はリポジトリを作って、その中でライブラリを管理しますので、例えばブランチごとに Maven リポジトリを分けて管理するといったことが必要になります。

言語的な差

変数宣言

ハッシュを宣言する場合のサンプルです。

PHP では 5.4 からarray()ではなく[]で配列(下のコードは連想配列)を定義することができるようになりシンプルな記述が可能です。

1<?php
2$ids = [
3    1 => 'user_a',
4    2 => 'user_b',
5];
6

すっきり。

Java では型宣言が必要なため、コード量が長くなってしまいます。

1// Java
2Map<Integer, String> map = new HashMap<Integer, String>();
3map.put(Integer.valueOf(1), "user_a");
4map.put(Integer.valueOf(2), "user_b");

PHP に慣れていると、Java の書き方がツラいなと感じる人は多いでしょう。
コード補完をうまく使いましょう。

また、Java 5 からオートボクシングが使える、Java 7 から右オペランドをnew HashMap<>()と省略できるようにはなり、書きやすくなってきてはいます。

余談ですが、こんなコードを見ると、古き良き時代の Java だなぁと感じます。

1StringBuilder  sb = new StringBuilder();
2BufferedReader br = new BufferedReader(new InputStreamReader(is));

暗黙の型変換

Java は静的型付けであり、文字列はあくまでも文字列として扱われます。
当然ですが。当たり前ですが。

1// java
2String strAge = "20";
3strAge = strAge + 1; // "20"+ "1" = 201

これに対し、PHP は動的型付けで_勝手に型を変えて処理_してしまうため、文字列が数字として扱われてしまうということがあります。

1<?php
2$age = "20";
3var_dump($age);  // string(2) "20"
4
5$age = $age + 1;
6var_dump($age);  // int(21) <= 文字列20 + 数値1  201じゃないの?
7

文字列 “20” が整数として扱われ、「 20+1 = 21 」という計算を行っています。 PHPer にしか理解できない挙動ですね。。。

では、文字列と文字列を演算してみます。

1<?php
2$age = "20";
3$add = "1";
4$age = $age + $add;
5var_dump($age);  // int(21)
6

今度も 21 となりました。

$age に文字列 “a” を代入した場合どうでしょうか。

1<?php
2$age = "a";
3$age = $age + 1;
4var_dump($age);      // int(1) <= 文字列a + 数値1
5

“a” は数値として解釈できないので、0として扱われてしまいました。

これが暗黙の型変換です。
恐ろしいですね。

Javaで「文字列20 + 数値1」を 21 に導くためには、文字列を数値に変換します。

1// java
2String strAge = "20";
3int age = Integer.parseInt(strAge);
4age = age + 1; // 20 + 1 = 21

「自動で型変換が変換される」のは初心者に優しいだけで、もちろん バグの温床 となります。

何がどうなるかをしっかり理解していれば対処可能ですが、対処が漏れても何となく動いてしまうため、厄介です。 PHPでは動的型付けを発生させないように 型を意識するコーディング が大切です。

次のように、1 つの変数に異なる型の値を代入できることも、コードを汚してわかりにくい不具合につながってしまいますね。

1<?php
2// アンチパターン
3$tmp = true;
4$tmp = "userName";
5

比較

PHP では文字列をそのまま比較しますが、Java では当然アウトな実装です。

1<?php
2$strA = "AAA";
3$strB = "AA";
4$strB = $strB . "A";
5
6if ( $strA == $strB ) // true
7
1// Java
2String strA = "AAA";
3String strB = "AA";
4strB = strB + "A";
5
6if (strA == strB) // false
7
8if (strA.equals(strB)) // true

また、Java で Integer を比較する場合はintValue()を使用します。

1Integer intA = new Integer(1);
2Integer intB = new Integer(1);
3
4if (intA == intB) // false
5
6if (intA.intValue() == intB.intValue()) // true

上のコードの挙動は問題ないと思います。

しかし、大なり小なりで比較するときは次のような挙動となります。

1Integer intA = new Integer(1);
2Integer intC = new Integer(2);
3
4if (intA == intC) // false
5
6if (intA < intC)  // true
7
8if (intA > intC)  // false

== 比較の比較はオブジェクトを比較し、> < の比較はアンボクシングされているのでしょう。
時々 intValue() を省略する実装を目にしますが、わかりにくい動きなので、数値の比較は intValue() を明記し、プリミティブ型で比較を行うのが良いと考えています。

戻り型が不定

PHP は「返り値の型」指定が必須ではありません。
( Java では戻り型?)

そのため次のようなことができてしまいます。

 1<?php
 2// アンチパターン
 3public function foo($var)
 4{
 5    if ( [条件式1] ) {
 6        return "result";
 7    }
 8    if ( [条件式2] ) {
 9        return array();
10    }
11}
12
  • [条件式1] が真の場合は文字列を返す
  • [条件式2] が真の場合は配列を返す
  • [条件式1]、[条件式2] の両方が偽の場合は何も返却しない (void)

と、それぞれ異なる型を返却することができます。

“暗黙の型変換”と同様に、「期待している型」と「返却された型」が一致せずに不具合につながってしまいます。

これも Java ではありえない挙動です。

1// java
2public void foo() {
3    return "foo"; // void メソッドは値を戻すことができません
4}

PHP7 になって型宣言(タイプヒンティングとも呼ばれる) が強化され、ようやくここで返り値の型を指定できるようになりました。

1<?php
2static function now(): DateTime
3{
4    return new DateTime();
5}
6

PHP 5.6 以前のタイプヒントは引数のみ、型は array、クラス型のみのです。 PHP 7 にぜひ移行しましょう。

参考

PHP > マニュアル > 言語リファレンス > 関数 > 返り値
http://php.net/manual/ja/functions.returning-values.php

PHP > マニュアル > 言語リファレンス > 関数 > 関数の引数
http://php.net/manual/ja/functions.arguments.php

配列、連想配列

とりあえず何でも配列に入れる という実装が簡単にできてしまうのが PHP の怖いところ。

 1<?php
 2
 3function foo(array $a)
 4{
 5    // 何か処理
 6}
 7
 8foo([ 1, 2, 3 ]);
 9
10foo([
11    "key1" => "value1",
12    "key2" => "value2",
13]);
14
15foo([
16    "key1" => [ 1, 2, 3 ],
17    "key2" => "value2",
18]);
19

引数 array で型宣言していますが、これらはすべて処理が通ります。

ここから PHP の配列が スカラー配列と連想配列の区別はなく、配列の内部構造がどうなっているかも考慮しない ということがよくわかります。

公式にも「 PHP の配列は、実際には順番付けられたマップです 」という記述があります。

PHP > マニュアル > 言語リファレンス > 型
http://php.net/manual/ja/language.types.array.php

順序付きマップですからループしたときに 順序がしっかり保証される のですね。

1<?php
2$array = [
3  "foo" => 1,
4  "var" => 2,
5];
6foreach ($array as $key => $value) {
7  echo "{$key} => {$value}";  // 必ず foo, varの順序で出力される
8}
9

Java で、PHP でいう連想想配と近いものがハッシュです。

1// Java
2Map<Integer, String> map = new HashMap<>();
3map.put(1, "user1");
4map.put(2, "user2");
5
6for (Map.Entry<Integer, String> e : map.entrySet()) {
7    System.out.println(e.getKey() + " : " + e.getValue());
8}

次は配列。
Java は配列宣言時に要素数を指定します。

1// Java
2int[] intArray = {1, 2, 3};

PHP の配列は後からいくらでも要素を追加できるので、PHPer はまずこの配列の扱いで転びます。

Listは柔軟に add() できるため、PHP に近い動きをしてくれます。
拡張for文も扱いやすいです。

1// Java
2List<Integer> intList = new ArrayList<>();
3idList.add(1);
4idList.add(2);
5
6for (Integer i : intList) {
7  // 何か美しい処理
8}

タイムスタンプ

1<?php
2echo time();  // 1451574000
3
1// Java
2Date dt = new Date();
3System.out.println(dt.getTime()); // 1451574000000

PHP では秒単位であるのに対し、Java ではミリ秒単位でした。
よく忘れます。

例外処理

java では例外をスローする場合に未対処であれば、しっかり教えてくれます。

1// Java
2File file = new File(fileName);
3FileReader filereader = new FileReader(file);
4// 処理されない例外の型 FileNotFoundException

PHP でtry-catchが実装されたのは PHP 5 以降ということで、遅いですね。
それまでは WarningやFatalといったエラーが発生してそこで処理がストップ してしまい、後処理をすることすら許してくれません。

書籍やネットのサンプルコードでも、しっかりと例外処理されていないものが多いため注意が必要です。

たとえば file_get_contents() に URL を指定すればネット上のコンテンツを拾うことができますが、ネット接続に失敗したときには false が返却されるだけです。

また、PHP では 関数の前に @ をつけて、エラーを無視してしまうというかなり危険な処理 も可能です。

1<?php
2$content = @file_get_contents($url);
3

PHPの実装が簡単にダメになる理由

配列での実装が楽なので何でも配列になりがち

本当にPHPでは配列ばかり使います。

たとえば、数年前に書かれたコードを読むと、DB 問い合わせ結果を連想配列に fetch する実装 が多いことに気が付きます。

1$query  = $this->db->query($sql, $params);
2$result = $query->fetchAll();
3

DB 問い合わせ結果を print_r(); すると、次のような構造の配列になっています。

 1Array
 2(
 3    [0] => Array
 4        (
 5            [id]   => 1
 6            [name] => user1
 7        )
 8
 9    [1] => Array
10        (
11            [id]   => 2
12            [name] => user2
13        )
14
15)

「スカラー配列の中に連想配列が入っている」という構造ですね。
前述のとおり、 スカラー配列も連想配列も両方array であるため、これでは PHP の型宣言などで区別ができません。

次のコードは、過去非常によくお目にかかった PHP の実装です。

 1<?php
 2function getCardMst($id=null)
 3{
 4    $records = MCard::getInstance()->findAll();
 5    if ( is_null($id) )
 6    {
 7        // array( array(), array(), array()..  )
 8        return $records;
 9    }
10    else
11    {
12        // array()
13        return $records[$id];
14    }
15}
16

問題なのは 引数の有無で返り値の構造が変わってしまう ことです。
期待している返り値が得られなかった場合に意図しない動作となってしまいますね。

上記の PHP コードを Java で書き直したサンプルが以下です。

1// Java
2public List<MChara> findAll() {
3    return MChara.dao().findAll();
4}
5
6public MChara findOne(int id) {
7  return MChara.dao().findOneById(id);
8}

こちらは返り値の構造が一目瞭然であり、齟齬が少ないですね!

PHP でも、新しめの実装は ORM を活用し、DB 問い合わせ結果をクラスにマッピングしてくれるものがありますので、そういったものを活用しましょう。
Laravel はそうなってました。

PHPで返り値がどんどん汚れる例

 1<?php
 2function getRanking()
 3{
 4    return array(
 5        1 => 'user_a',
 6        2 => 'user_b',
 7        :
 8    );
 9}
10

ランキングの順位とユーザ名を返却するシンプルな関数があったとします。

「あ、ユーザIDも必要だな」ということで、ユーザ ID を追加しました。
コードはこうなります。

 1<?php
 2function getRanking()
 3{
 4    return array(
 5        1 => array( 'name' => 'user_a', 'id' => 1111),
 6        2 => array( 'name' => 'user_b', 'id' => 2222),
 7        :
 8    );
 9}
10

「あ、ランキングの集計時間も欲しいな。」ということで、集計時間のデータを追加しました。

 1<?php
 2function getRanking()
 3{
 4    return array(
 5        "time" => "2015-01-01 17:17:00",
 6        "list" => array(
 7            1 => array( 'name' => 'user_a', 'id' => 1111),
 8            2 => array( 'name' => 'user_b', 'id' => 2222),
 9            :
10        ),
11    );
12}
13

“集計時間”をどうやって返却するか悩んだ挙句、返り値の構造を大きく変えてしまいました。

運用フェーズに入って時間に追われるようになると、大真面目にこういった修正をやってしまいます。
だって、言語レベルでそれができちゃう から。

返り値が異なりますので、関数の呼び出し元も修正しなければなりません。
しかしコンパイルすることがありませんから、よく見落とし、サーバ上で動かした後に不具合が発覚します。

こんなことで不具合出しちゃうとか本当にもったないです。


Java では返り値の型を定義しなくてはなりません。
もし返り値の型や構造が変わる場合は呼び出し元も変更しなければコンパイルも通りません。

初期状態のコードをこのように実装したとします。

1// Java
2public Map<Integer, String> getRanking()
3{
4    Map<Integer, String> map = new HashMap<>();
5    map.put(1, "user_a");
6    map.put(2, "user_b");
7    :
8    return map;
9}

「ユーザIDの返却が必要」となった場合、小さなクラスを用意して Map の中身をそのクラスにすることで、きれいで把握しやすい構造とすることができますね。

 1// Java
 2public class RankingUser
 3{
 4    private String  name;
 5    private Integer id;
 6
 7    public RankingUser(String name, Integer id) {
 8         this.name = name;
 9         this.id   = id;
10    }
11    :
12}
13
14public class FooService
15{
16    public Map<Integer, RankingUser> getRanking()
17    {
18        Map<Integer, RankingUser> map = new HashMap<>();
19
20        map.put(1, new RankingUser("user1", 1111));
21        map.put(2, new RankingUser("user2", 2222));
22        :
23
24        return map;
25    }
26}

おわりに

PHP は言語レベルでの制約が緩いため、簡単にコードを汚すことができます。

結果的にそれが修正コストを跳ね上げ、プロジェクトをコードを収集不可能な事態に追い込むことがありますから、不具合が減らせられるように意識したコーディングが大切です。

そのポイントは次にまとめられます。

  • 返り値の型を固定する
  • 配列で済まさずに、小さいクラスを構造体として用いる
  • しっかり例外対応を行う

PHP の悪いところをしっかり把握して実装でカバーしようという内容です。
つまり PHPじゃなくてよくはないだろうか ってことになりますね。

また、実装中は「Java くそー」と思うことも多々あったのですが、Java の良いところだけ見ているような内容になってしまいました。
何か思い出したら書き足します。

改訂2版 パーフェクトJava

井上 誠一郎,永井 雅人
出版社:技術評論社  発売日:2014-11-01

Amazonで詳細を見る

プログラミングPHP 第3版

Kevin Tatroe,Peter MacIntyre,Rasmus Lerdorf
出版社:オライリージャパン  発売日:2014-03-25

Amazonで詳細を見る

PHP+MySQLマスターブック

永田 順伸
出版社:マイナビ  発売日:2014-01-24

Amazonで詳細を見る